Skip to content

~8 min readgrounded in docs/DEPLOY.md · scripts/bootstrap-cloudflare.mjs · apps/web/vercel.json

Deploy in 15 minutes·

This is the sequence that produced the live demo: one script for Cloudflare, one wrangler deploy, one Vercel project. Every command was run, not paraphrased; the engineering copy with timings is docs/DEPLOY.md.

Before you start

Cloudflare account (free plan), Vercel account, GitHub account, an OpenAI API key, Node 22. Total wall-clock: about 15 minutes, of which 2 are the one step no CLI can do (the GitHub OAuth App).

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

Under your account, and only with that prefix, the script creates:

Resource Count Used for
KV namespaces tiny, post, stats 3 tiny: the tiny record cache and sweep/alarm stamps · post: the per-conversation ring shared by consulted tinys (1 h TTL) · stats: create/update counters
D1 database 1 every table — --migrate applies the 36 migrations
Vectorize indexes 2 1536-dim cosine memory search, with the name / userId metadata indexes the memory queries filter on
R2 bucket 1 media

It writes apps/worker/wrangler.generated.toml with the real ids (git-ignored) and lists everything it made in docs/PROVISIONED.md. Re-running is safe: existing resources are reused. --dry-run prints the wrangler commands instead of running them.

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}

3 · GitHub OAuth App 2 min · the only manual step·

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

  • Homepage URL: https://<your-project>.vercel.app — you pick the 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·

Click Deploy with Vercel on the Home page or in the README. The form asks for the ten variables in the table below; the root directory (apps/web) and build settings come from apps/web/vercel.json.

npx vercel project add tiny-vercel
npx vercel link --yes --project tiny-vercel
# root directory is a project setting with no CLI flag — 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 of the 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, GITHUB_CLIENT_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 check it; unset means nobody is owner
OPENAI_API_KEY (+ TINY_MODEL_PROVIDER) default chat model; users may bring their own keys later
NEXT_PUBLIC_SITE_NAME header, Open Graph cards, web manifest

Everything else in .env.example is optional — see Environment variables.

5 · Prove the round trip 2 min·

The app needs to know its own public origin (WebAuthn relying-party id, OAuth return, absolute links), and the worker needs to know where the app lives (share links, firmware pointers).

printf '%s' https://<your-project>.vercel.app | npx vercel env add NEXT_PUBLIC_APP_URL production --force
npx vercel redeploy <your-project>.vercel.app
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 — Vercel KV / Upstash (KV_REST_API_URL, KV_REST_API_TOKEN). Unset = unlimited.
  • Web Push — npx web-push generate-vapid-keys; the public half goes to the app as NEXT_PUBLIC_VAPID_KEY, both halves plus VAPID_SUBJECT to the worker.
  • Email in — Cloudflare Email Routing → EMAIL_OWNER_FORWARD on the worker.
  • Payments — PAYMENTS_ENABLED=true on both app and worker plus the keys in docs/ENV.md. Off by default; every money route answers 404 until then.

If something fails·

Symptom Cause Fix
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 non-members. The CLI shows UNKNOWN and hangs commit as a team member, or deploy from a Git connection. vercel ls and the REST /v6/deployments endpoint show the real reason
TINY_WORKER_URL not set in build logs, or workerConfigured:false the variable is missing on the environment you deployed to vercel env ls production
every worker call is a 401 INTERNAL_API_KEY differs between the app and wrangler secret put set the same value on both sides
Vectorize: filter on unindexed property the metadata indexes were not created re-run bootstrap-cloudflare.mjs — idempotent, it adds them

Tear down·

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

Rehearsed in both directions on the demo account: teardown takes about 9 s (worker, 2 Vectorize, R2, D1, 3 KV — exactly the 8 prefixed rows), bootstrap + migrate about 19 s, secrets + deploy about 14 s. --keep-data deletes only the worker. Every wrangler delete's output is appended to .cloudflare-teardown.log (git-ignored).