Environment variables
Env is schema-driven with varlock. Every key the apps read is declared once, with its type, whether it is secret, and whether it is required, in a committed .env.schema; values come from the schema’s public defaults, a gitignored .env.local, or the platform (Cloudflare, Fly, CI). bunx varlock load names anything missing or malformed before an app boots; bunx varlock load --agent prints the resolved env with secrets redacted, which is the only way an agent should inspect it.
.env.schema # shared: Supabase, durable streams, PostHog, LLM keys, Daytona, Giphyapps/www/.env.schema # + OpenRouter management key, feedback key, release identityapps/backstage/.env.schema # + DATABASE_URL, director overrides, worker idpackages/sandbox/src/daytona/proofs/.env.schema # + proof knobs (stream url, house/thread ids).env.local # gitignored secrets for every app (also apps/<app>/.env.local for app-only ones)Each app schema imports the root directory (@import(../../)), so the root schema’s defaults and the root .env.local reach every app; an app’s own .env.local wins over both. Secrets are anything not prefixed PUBLIC_ unless the schema marks it @public. The environment flag is APP_ENV, resolved from the platform when unset (main → production, PRs → preview, vitest → test, laptop → development); the backstage image sets it to production.
App code reads the generated package-local ENV: $lib/env in www, ./env.js in backstage. Each app uses @generateTsTypes(exposeEnv=local, auto=false) so its types cannot merge with another app’s schema. After editing a schema, run bun run env:generate and commit the generated modules. Generation runs once per app using an explicit schema path; multiple -p flags merge schemas rather than generating independent types. Formatters leave the generated output alone.
How each surface loads it:
- www dev/build —
@varlock/vite-integrationinapps/www/vite.config.tsvalidates the schema, initializesENV, and rejects sensitive values referenced from client code. Build provenance stays pinned to the source revision; analytics release identity is resolved at runtime. - www prod (Cloudflare) —
apps/www/scripts/deploy-worker.tsdeploys withvarlock-wrangler, not plainwrangler: it resolves the schema from the Builds environment, uploads non-sensitive values as worker vars and sensitive ones as secrets, and injects the__VARLOCK_ENVblob the SSR loader reads at boot. Set secrets in the Cloudflare dashboard under Workers Builds → Variables and Secrets, not the Worker’s own Secrets tab. Cloudflare doesn’t expose Worker runtime secrets to build jobs, so the Builds store is the only one varlock can read; the Worker’s Secrets tab is managed output that every deploy overwrites, and a value set only there disappears on the next push. Required secrets must exist in Builds for validation to pass. There are no hand-managed wrangler secrets; values not declared in the schema are dropped on deploy. Secrets are still runtime bindings on the Worker, nothing is baked into the bundle.RELEASE_SHAis put in the process env by the deploy script so varlock uploads it. PR previews need the same treatment: the non-production Builds command must bebun run --filter '@arbe/www' deploy:preview, not plainwrangler versions upload. - backstage prod (Fly) — Fly supplies secrets; the image includes only committed schemas and validates them through
varlock runbefore starting the worker.varlock/auto-loadthen hydrates the JavaScriptENVproxy from the validated blob.varlock runalone injects process values, not the proxy. Local secret files are excluded from the image. Do not start a second backstage locally: it would claim production jobs. - scripts —
bunx varlock run -- bun run <script>from the directory whose schema applies (repo root for most; the proofs dir forprove-*.ts). Bun’s own.envautoload is off at the root (bunfig.toml). - CI —
bun run check:envregenerates the app ENV modules and validates root, www, and backstage schemas before the builds, with placeholder secrets from the workflow env.
Gotchas that the schema comments also carry: SUPABASE_JWT_SECRET is the raw HS256 secret from Supabase → Settings → API → JWT Secret, not a JWT and not the service role key (wrong value → “No suitable key”). The web push keys (PUBLIC_VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY) must be the same pair locally, on www and on Fly: www pushes for people’s posts, Fly for bot replies, and a browser’s subscription only accepts pushes signed by the key it subscribed with; unset leaves @mention notifications off. PostHog must be set on www and Fly, or HTTP-side spans stay visible while chat $ai_generation spans disappear (analytics). LLM keys (OPENROUTER_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY) are overridden per house by a house secret of the same name; GITHUB_TOKEN is only ever a house/sandbox secret. RELEASE_SHA is a Varlock-managed runtime binding, and ARBE_RELEASE_SHA takes precedence over Cloudflare’s WORKERS_CI_COMMIT_SHA when deploying from a source archive.
Secret storage: keep shared secrets in the root .env.local and app-only ones (such as backstage’s DATABASE_URL) in that app’s .env.local. If a legacy .env holds secrets, the human moves its values into .env.local, validates, and removes .env; there is no need to keep both. Encrypt every local value file, including any environment-specific overrides: encrypting the root file does not encrypt app-local copies.
From each schema’s directory, the human runs:
bunx varlock encrypt --file .env.localbunx varlock load --agentUse separate cd commands to change directories; Bash-style (cd ... && ...) grouping does not work in fish. Encryption is device-bound; use bunx varlock reveal <NAME> when a value is needed, run from the directory whose schema declares the key (the repo root for shared keys, apps/www for www-only ones like OPENROUTER_MANAGEMENT_KEY) — elsewhere it reports “not found in schema”. Agents never read or edit these value files. bunx varlock explain <NAME> safely shows the value’s source with sensitive values redacted.
Code: .env.schema and the three app schemas, apps/www/vite.config.ts, apps/www/src/hooks.server.ts, apps/backstage/src/index.ts.
See system/deployment, system/development, system/auth, system/environments.