# Environment variables

Env is schema-driven with [varlock](https://varlock.dev). 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).

How each surface loads it:

- **www dev/build** — `@varlock/vite-integration` in `apps/www/vite.config.ts` validates the schema and injects the resolved env before SvelteKit reads `$env/static/*` and `$env/dynamic/*`. `hooks.server.ts` still re-checks the six core vars at module load.
- **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. Keys read via `$env/static/private` must still exist as Builds secrets or the build fails with `MISSING_EXPORT`; `OPENROUTER_API_KEY` is read both ways. The worker's runtime env now comes from the schema, so there are no hand-managed wrangler secrets; varlock also passes `--keep-vars=false`, dropping any var not in the schema. `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)** — every value is a Fly app secret; the Docker entrypoint is plain `bun run`, so the schema is not consulted there yet. `apps/backstage/src/index.ts` names missing vars at boot. Locally `bun run start` goes through `varlock run`.
- **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` 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"). PostHog must be set on www *and* Fly, or HTTP-side spans stay visible while chat `$ai_generation` spans disappear ([analytics](../ops/analytics.md#querying)). 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 wrangler `--var`, and `ARBE_RELEASE_SHA` takes precedence over Cloudflare's `WORKERS_CI_COMMIT_SHA` when deploying from a source archive.

Secret storage: plaintext `.env.local` values can be encrypted in place with `bunx varlock encrypt --file .env.local` (device-bound, revealed with `bunx varlock reveal <NAME>`).

Code: `.env.schema` and the three app schemas, `apps/www/vite.config.ts`, `apps/www/src/hooks.server.ts`, `apps/backstage/src/index.ts`.<br>
See [system/deployment](../ops/deployment.md), [system/development](../ops/development.md), [system/auth](../access/auth.md), [system/environments](environments.md).
