Skip to content

~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.