Skip to content

Deploy tiny-vercel in 15 minutes·

This is the exact sequence that produced the live demo (https://tiny-vercel-scratch.vercel.app) on 2026-09-15: one script for Cloudflare, one wrangler deploy, one Vercel project. Every command below was run, not paraphrased — see docs/PROVISIONED.md for what it created.

You need: a Cloudflare account (free plan is enough — D1, KV, Vectorize, R2 and Workers all have free tiers), a Vercel account, a GitHub account, an OpenAI API key (embeddings + the default model; any other provider in docs/ENV.md works for chat, embeddings stay on text-embedding-3-small).

0 · Clone and install (1 min)·

git clone https://github.com/cagataycali/tiny-vercel tiny-vercel && cd tiny-vercel
npm ci
npx wrangler login          # browser; grants the CLI your Cloudflare account
npx vercel login            # browser

1 · Cloudflare storage (2 min)·

node scripts/bootstrap-cloudflare.mjs --prefix tiny-vercel --migrate

Creates, under your account and only with that prefix: 3 KV namespaces (tiny, post, stats), 1 D1 database (36 migrations applied by --migrate), 2 Vectorize indexes (1536-dim cosine, plus the name/userId metadata indexes the memory queries filter on) and 1 R2 bucket. Writes apps/worker/wrangler.generated.toml with the real ids (git-ignored) and lists everything in docs/PROVISIONED.md. Re-running is safe: existing resources are reused. --dry-run prints the wrangler commands instead. Undo with node scripts/teardown-cloudflare.mjs --yes.

2 · Worker secrets and deploy (2 min)·

cd apps/worker
openssl rand -hex 32 > /tmp/internal-key            # keep it: the app needs the same value
npx wrangler secret put INTERNAL_API_KEY --config wrangler.generated.toml < /tmp/internal-key
npx wrangler secret put OPENAI_API_KEY   --config wrangler.generated.toml   # paste the key
npx wrangler deploy --config wrangler.generated.toml
cd ../..

The deploy prints https://tiny-vercel-worker.<you>.workers.dev. Check it: curl https://tiny-vercel-worker.<you>.workers.dev/health → {"ok":true,"service":"worker",…,"paymentsEnabled":false}.

Rehearsed: undo and redo (2026-09-15)·

The scripts were proven in BOTH directions on the demo account, twice in a row:

node scripts/teardown-cloudflare.mjs --yes        # 9s: worker, 2 vectorize, r2, d1, 3 kv — exactly the 8 prefixed rows; the Vercel project and every non-prefixed resource untouched
node scripts/bootstrap-cloudflare.mjs --prefix tiny-vercel --migrate --app-url https://<app>   # 19s, 36 migrations
# secrets + wrangler deploy as in step 2                                                       # 14s

33 s from an empty account to a serving worker. Nothing on the Vercel side changed — the app only knows the worker by its name-based workers.dev URL, so a rebuilt worker is reachable at the same address and TINY_WORKER_URL stays valid. What you lose is the data: D1 rows, KV, memories, media (the D1 ids restart at 1). Vectorize is eventually consistent — a freshly stored memory shows in learnings at once and in relevant (semantic recall) after ~20 s.

--keep-data deletes only the worker (for a redeploy with fresh bindings). Every wrangler delete's full output is appended to .cloudflare-teardown.log (git-ignored); if wrangler exits non-zero the script re-lists the resource and believes Cloudflare, not the exit code.

3 · GitHub OAuth App (2 min, the only step no CLI can do)·

https://github.com/settings/developers → New OAuth App:

  • Homepage URL: https://<your-project>.vercel.app (you can pick the Vercel project name in step 4 — it becomes <name>.vercel.app)
  • Authorization callback URL: https://<your-project>.vercel.app/api/auth

Keep the client id and generate a client secret.

4 · Vercel project (5 min)·

Button: click Deploy with Vercel in the README. The form asks for the variables below; the root directory (apps/web) and build settings come from apps/web/vercel.json.

CLI equivalent (what produced the demo):

npx vercel project add tiny-vercel
npx vercel link --yes --project tiny-vercel
# root directory is a project setting; the CLI has no flag for it — set it once in the dashboard
# (Settings → General → Root Directory → apps/web) or with the API:
#   curl -X PATCH https://api.vercel.com/v9/projects/tiny-vercel -H "Authorization: Bearer $VERCEL_TOKEN" \
#        -H 'content-type: application/json' -d '{"rootDirectory":"apps/web","framework":"nextjs","nodeVersion":"22.x"}'
add() { printf '%s' "$2" | npx vercel env add "$1" production --force; }
add TINY_WORKER_URL             https://tiny-vercel-worker.<you>.workers.dev
add NEXT_PUBLIC_TINY_WORKER_URL https://tiny-vercel-worker.<you>.workers.dev
add INTERNAL_API_KEY            "$(cat /tmp/internal-key)"
add AUTH_JWT_SECRET             "$(openssl rand -hex 32)"
add ENROLL_SECRET               "$(openssl rand -hex 32)"
add GITHUB_CLIENT_ID            <from step 3>
add GITHUB_CLIENT_SECRET        <from step 3>
add OWNER_LOGIN                 <your GitHub login>
add NEXT_PUBLIC_SITE_NAME       "tiny-vercel"
add TINY_MODEL_PROVIDER         openai
add OPENAI_API_KEY              <key>
npx vercel deploy --prod --yes
Variable Why
TINY_WORKER_URL, NEXT_PUBLIC_TINY_WORKER_URL the worker from step 2 (server-side and browser-side copies, same origin)
INTERNAL_API_KEY must equal the worker secret — every internal worker call is refused otherwise
AUTH_JWT_SECRET signs the tiny_session cookie
ENROLL_SECRET HMAC for device enrollment codes
GITHUB_CLIENT_ID/SECRET first login; passkeys and CLI tokens are issued afterwards
OWNER_LOGIN your GitHub login(s), comma-separated — owner-only routes such as the device UDID roster; unset = nobody is owner (fail closed)
OPENAI_API_KEY (+ TINY_MODEL_PROVIDER) default chat model; users may bring their own keys later
NEXT_PUBLIC_SITE_NAME header, OG cards, manifest

Everything else in .env.example is optional and documented in docs/ENV.md.

5 · Close the loop (2 min)·

# tell the app its own public origin (WebAuthn rpID, OAuth return, absolute links) and redeploy
printf '%s' https://<your-project>.vercel.app | npx vercel env add NEXT_PUBLIC_APP_URL production --force
npx vercel redeploy <your-project>.vercel.app
# tell the worker where the app lives (share links, firmware pointers) and redeploy it
node scripts/bootstrap-cloudflare.mjs --app-url https://<your-project>.vercel.app --deployed https://tiny-vercel-worker.<you>.workers.dev
(cd apps/worker && npx wrangler deploy --config wrangler.generated.toml)

6 · Verify·

A=https://<your-project>.vercel.app
curl -s $A/api/health          # {"ok":true,"service":"web","workerConfigured":true,"appUrlConfigured":true,"paymentsEnabled":false}
curl -s $A/api/events          # {"error":"login required"} — 401, fail-closed
curl -s -N -X POST $A/api/chat -H 'content-type: application/json' -H 'x-tiny-name: tiny' -H 'x-tiny-session: s1' \
     -d '{"messages":[{"role":"user","content":"Reply with exactly the word: pong"}]}'   # SSE … "textDelta":"pong" … [DONE]

Then in a browser: sign in with GitHub → Create your AI → chat → Settings → add a passkey → sign out and back in with the passkey → Devices → enroll this laptop with npx tiny-vercel init <your origin> then npx tiny-vercel login.

Optional switches·

  • Rate limiting — add Vercel KV / Upstash (KV_REST_API_URL, KV_REST_API_TOKEN); unset = unlimited.
  • Web Push — npx web-push generate-vapid-keys; public half to the app (NEXT_PUBLIC_VAPID_KEY), both halves + VAPID_SUBJECT to the worker.
  • Email in — Cloudflare Email Routing → EMAIL_OWNER_FORWARD on the worker (docs/ENV.md §email).
  • Payments — PAYMENTS_ENABLED=true on both app and worker plus the keys in docs/ENV.md §payments. Off by default; every money route answers 404 until then.

Troubleshooting·

  • BLOCKED — the commit author doesn't have permission to create deployments — CLI deploys carry the HEAD commit's author; a team with Git author permission checks refuses authors who are not members (we hit this with a bot identity tiny <tiny@tiny.technology>). Commit as a team member or deploy from a Git connection. The CLI shows the deployment as UNKNOWN and hangs; vercel ls + the REST /v6/deployments endpoint show the real reason.
  • TINY_WORKER_URL not set in build logs / workerConfigured:false — the env var is missing on the environment you deployed to (vercel env ls production).
  • Every worker call 401 — INTERNAL_API_KEY differs between the app and wrangler secret put.
  • Vectorize: filter on unindexed property — the metadata indexes were not created; re-run bootstrap-cloudflare.mjs (idempotent, adds them).

Tear down·

node scripts/teardown-cloudflare.mjs --yes      # deletes exactly what docs/PROVISIONED.md lists
npx vercel project rm tiny-vercel