~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·
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 asNEXT_PUBLIC_VAPID_KEY, both halves plusVAPID_SUBJECTto the worker. - Email in — Cloudflare Email Routing →
EMAIL_OWNER_FORWARDon the worker. - Payments —
PAYMENTS_ENABLED=trueon both app and worker plus the keys indocs/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).