Skip to content
View as .md

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, Giphy
apps/www/.env.schema # + OpenRouter management key, feedback key, release identity
apps/backstage/.env.schema # + DATABASE_URL, director overrides, worker id
packages/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-integration in apps/www/vite.config.ts validates the schema, initializes ENV, 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.ts deploys with varlock-wrangler, not plain wrangler: it resolves the schema from the Builds environment, uploads non-sensitive values as worker vars and sensitive ones as secrets, and injects the __VARLOCK_ENV blob 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_SHA is 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 be bun run --filter '@arbe/www' deploy:preview, not plain wrangler versions upload.
  • backstage prod (Fly) — Fly supplies secrets; the image includes only committed schemas and validates them through varlock run before starting the worker. varlock/auto-load then hydrates the JavaScript ENV proxy from the validated blob. varlock run alone 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 for prove-*.ts). Bun’s own .env autoload is off at the root (bunfig.toml).
  • CI — bun run check:env regenerates 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:

Terminal window
bunx varlock encrypt --file .env.local
bunx varlock load --agent

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