Environment variables
One contract, validated at startup, reporting every problem at once and naming where each variable comes from.
Every variable the platform reads is declared in one place, together with its type, whether it is required, and its source — the integration, the storage store, or the deploy prompt. Validation reports every problem at once rather than failing at the first:
Environment is not usable:
- SUPABASE_URL is required (from the Supabase integration) but is empty
- POSTGRES_URL is required (from the Supabase integration) but is empty
- DEMO_USER_PASSWORD is required (from the Deploy Button prompt) but is emptyThe ones you will actually meet
| Variable | Source | Purpose |
|---|---|---|
SUPABASE_URL, SUPABASE_SECRET_KEY, SUPABASE_PUBLISHABLE_KEY, SUPABASE_JWT_SECRET | Supabase integration | Auth and admin access |
POSTGRES_URL | Supabase integration | The pooled connection used by request-path work |
POSTGRES_URL_NON_POOLING | Supabase integration | Session-mode work, such as migrations |
BLOB_READ_WRITE_TOKEN | Blob store | Asset bytes |
DEMO_USER_PASSWORD | Deploy Button prompt | The administrator created by the first build |
CMS_LOCAL_BLOB_DIR | Local development only | Store asset bytes on disk instead of in Blob |
CMS_CORS_ORIGINS | Optional | Extra browser origins allowed to make credentialed requests |
Two rules
Never read the environment directly. Everything takes a parsed record, which keeps the parser pure and testable — and is required on the client, where only statically analysable public variables are inlined at all.
Never put a secret behind a public prefix. A lint rule enforces it: a public prefix inlines the value into the browser bundle, so a server-only secret documented under one would be published by a deployer doing exactly as they were told.
The example file
.env.example is the commented copy of the contract, and a check enforces both directions — a variable added to the contract but not documented fails the lint, and so does the reverse.
