~7 min readgrounded in docs/ENV.md · docs/DEPLOY.md · apps/web/.env.example · apps/worker/wrangler.toml · apps/worker/src/index.ts (Env) · apps/web/lib/{free-tier,rate-limit,site,config}.ts
Environment variables·
tiny-vercel has two deployables and two places to configure them: the app reads Vercel environment variables (apps/web/.env.example lists them all), the worker reads [vars] in wrangler.toml plus wrangler secret put. This page orders them by when you need them. The engineering ledger docs/ENV.md is the change log behind it.
The one rule that shapes the list: no code path carries a tiny.technology constant. Every host, brand string, key and address that the original platform hardcoded is a variable here, and most fail closed when unset rather than falling back to someone else's infrastructure.
1 · Make it work·
Without these, sign-in or chat does not happen. The deploy guide sets them in this order.
| Variable | Where | What it does |
|---|---|---|
INTERNAL_API_KEY |
app and worker secret | the shared secret behind every internal worker call (X-Internal-Key). Must be identical on both sides — a mismatch is every worker call answers 401. openssl rand -hex 32 |
TINY_WORKER_URL |
app | the worker's origin, used server-side. Unset: the app still builds, worker calls fail against tiny-worker-url-not-set.invalid, and /api/health reports workerConfigured: false |
NEXT_PUBLIC_TINY_WORKER_URL |
app | the browser-visible copy — chat, the command palette, the universe drawer and /tools fetch the worker's public /community, /get, /list, /tools/browse directly |
NEXT_PUBLIC_APP_URL |
app | your own public origin — WebAuthn rpID, the OAuth return, absolute links the agent hands out, the run-tool sandbox proxy, robots.ts / sitemap.ts / manifest.ts. Set it after the first deploy tells you the URL, then redeploy |
AUTH_JWT_SECRET |
app | signs the tiny_session cookie (HS256). Rotating it signs everyone out — browsers and devices — and breaks nothing else |
ENROLL_SECRET |
app | HMAC for device enrollment codes |
GITHUB_CLIENT_ID · GITHUB_CLIENT_SECRET |
app | the first sign-in. Passkeys and CLI tokens are issued afterwards and do not need GitHub again |
OWNER_LOGIN |
app | your GitHub login(s), comma-separated. Gates owner-only routes such as the device UDID roster. Unset means nobody is owner |
APP_URL |
worker [vars] |
the app's origin as the worker sees it — share links, vCard/QR URLs, "sign in at …" copy. Default http://localhost:3000; bootstrap-cloudflare.mjs --app-url writes it into wrangler.generated.toml |
2 · A model to answer with·
Set the provider you use; Configure models explains the three-layer resolution.
| Variable | Where |
|---|---|
TINY_MODEL_PROVIDER — openai · bedrock · gemini · gateway |
app |
OPENAI_API_KEY + OPENAI_MODEL_ID |
app; the worker also needs OPENAI_API_KEY as a secret — it embeds tinys and memories with text-embedding-3-small and runs /ask |
BEDROCK_MODEL_ID + AWS_REGION / BEDROCK_REGION + AWS_BEARER_TOKEN_BEDROCK |
app |
GEMINI_API_KEY / GOOGLE_API_KEY + GEMINI_MODEL_ID |
app |
AI_GATEWAY_API_KEY + AI_GATEWAY_MODEL_ID |
app |
STRANDS_ADDITIONAL_REQUEST_FIELDS (JSON) |
app, optional — passed through to the provider |
MODEL_CONFIG_ENC_KEY |
worker secret — AES-256-GCM key for users' own provider keys before they reach D1. Falls back to INTERNAL_API_KEY; set a dedicated one so rotating the internal key does not orphan every stored key |
3 · Make it yours·
Everything the original layout hardcoded — brand, handle, OG media, analytics, a Maps key — read by lib/site.ts and lib/config.ts.
| Variable | Meaning |
|---|---|
NEXT_PUBLIC_SITE_NAME |
brand in <title>, header, manifest, OG siteName. Default tiny |
NEXT_PUBLIC_SITE_TAGLINE |
description / OG / twitter text |
NEXT_PUBLIC_SITE_TWITTER |
X handle. Unset → no twitter card block at all |
NEXT_PUBLIC_SITE_OG_IMAGE (+ _WIDTH / _HEIGHT) |
OG image; default the bundled /icon-512.png. Dimensions must match the real asset |
NEXT_PUBLIC_VERCEL_ANALYTICS |
"1" mounts @vercel/analytics; unset, it is never imported |
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY |
the ambient map and /map. Unset: nothing renders |
NEXT_PUBLIC_FREE_TIER_REQUESTS_PER_DAY |
the signed-in free tier the UI quotes as "N requests a day". Default 50; a non-number or < 1 falls back to 50 rather than to zero |
TOOL_REPO_ALLOWLIST |
comma-separated GitHub owners whose raw tool files install_tool may fetch without a per-user trust grant. Default strands-agents |
WEATHER_API_KEY |
the built-in weather helper |
4 · Optional switches·
Each of these is a capability that stays off, safely, until you set it.
Rate limiting — KV_REST_API_URL + KV_REST_API_TOKEN (Vercel KV / Upstash) on the app. Unset = unlimited. With KV, a signed-in user gets their own daily window of the free tier plus 5 requests per reputation point, capped at 200 extra (community).
Web Push — VAPID_PUBLIC_KEY + VAPID_PRIVATE_KEY as worker secrets, optional VAPID_SUBJECT (default mailto:admin@<APP_URL host>); NEXT_PUBLIC_VAPID_KEY on the app is only a local-dev fallback — the browser fetches the key from the worker. Notifications.
Firmware pointers — WORKER_URL (worker, optional). /firmware/publish names an https URL a device will download code from; only the hosts of APP_URL and WORKER_URL are accepted. Neither set → every publish is refused.
Email in — EMAIL_OWNER_FORWARD (worker) is where tiny@ / postmaster@ / hello@ mail for your domain lands via Cloudflare Email Routing; unset, that mail is rejected. CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_API_TOKEN let the worker start destination verification when a forward fails. /health reports emailForwardConfigured.
Payments — PAYMENTS_ENABLED must be exactly "true" on both app and worker; anything else and every /pay/*, /api/wallet*, /api/x402/*, /api/chain/*, /api/erc8004/* answers 404 before touching a chain, and the cron skips the reconcilers. Then PAYMENTS_NETWORK (base · base-sepolia · tiny), DEPOSIT_ADDRESS, BASE_RPC_URL / BASE_SEPOLIA_RPC_URL, RECONCILE_ALARM_USER (worker), and on the app X402_PAY_ALLOWLIST, X402_FACILITATOR_URL, the TINY_CHAIN_* set for a self-hosted network, and the signer keys the routes document inline — refused when they are Anvil's well-known dev keys outside a dev network. Payments.
Worker bindings·
Not variables, but the same file. wrangler.toml declares — and bootstrap-cloudflare.mjs fills with real ids in wrangler.generated.toml:
| Binding | Type | Holds |
|---|---|---|
DB |
D1 | everything relational — users, tinys, devices, memory graph, jobs, events, payments |
tiny · post · stats |
KV | tiny records by slug and shares · archived conversations (with TTL) · counters |
VECTOR_INDEX · MEMORY |
Vectorize (1536-d cosine) | public tinys · per-user memories, with name / userId metadata indexes |
MEDIA |
R2 | media uploads, voice audio, transcripts, published firmware — served under unguessable keys via /api/media |
VOICE |
Durable Object | live voice sessions |
Checking a deployment·
curl -s https://<app>/api/health
# {"ok":true,"service":"web","workerConfigured":true,"appUrlConfigured":true,"paymentsEnabled":false}
curl -s https://<worker>/health
# {"ok":true,"service":"worker","relayPayloadMaxBytes":…,"paymentsEnabled":false,"paymentsNetwork":"base-sepolia","emailForwardConfigured":false}
Both report configuration gaps as booleans only — never the values.