Environment variables
Config file locations
Section titled “Config file locations”The stack reads three sets of environment files with this precedence order:
- apps/api/.env: consumed by the API process when running outside Docker
- compose/.env: injected into dev containers via
env_file:references - compose/api.prod.env: API-specific production overrides
In Docker Compose (the default), compose/.env is the single file you edit most often. It feeds into both app containers and the infra services (Postgres, Valkey, Traefik, observability).
Per-app usage
Section titled “Per-app usage”| Location | Consumed by | Format |
|---|---|---|
| apps/api/.env | API Bun process (outside Docker) | All vars |
| apps/ui/.env.local | Vite at build/dev time | VITE_* only |
| compose/.env | all services via docker-compose.yml | see docker-compose.yml env_file refs |
| compose/api.prod.env | API service in prod | API vars + STACK=prod-specific |
Critical variables (operator attention)
Section titled “Critical variables (operator attention)”These deserve special handling because they affect security, availability, or startup:
JWT_SECRET: Signs access cookies and refresh tokens. Rotation forces every user to sign in again. Generate with openssl rand -base64 48.
MFA_ENCRYPTION_KEY: AES-256-GCM key for TOTP secrets. Once users enrol, losing this key means every MFA user re-enrols from scratch. Generate with openssl rand -base64 32 and back up to hardware or paper.
POSTGRES_PASSWORD / VALKEY_PASSWORD: Data plane secrets. Rotate in production by updating the var, then restarting services. Database passwords should be 32+ random bytes. VALKEY_PASSWORD is required in production — the Valkey server itself enforces it via --requirepass, and the API’s env validator refuses to boot without it whenever queues, the Valkey cache, SSE notifications, or OAuth are enabled. Leaving it empty (the dev default) disables Valkey auth for local convenience only.
DATABASE_URL: Postgres connection string. Must include TLS mode for production (?ssl=require). In development defaults to localhost; in production, points to a managed provider or socket.
PUBLIC_UI_HOST: Hostname for the app in production, app.<domain> in the default split layout. Traefik uses this to request Let’s Encrypt certificates and routes /api/* and /health on this host to the API, everything else to the UI. Same-origin: the app and its API share one host, so no CORS and cookies stay on the app host. With WITH_SITE=0, set it to the apex instead to keep the single-domain layout.
FRONTEND_URL, PUBLIC_API_URL: In the split layout both are https://app.<domain>. FRONTEND_URL is where OAuth callbacks, Stripe return URLs and email links point. PUBLIC_API_URL is the origin only; the API appends /api/v1.
PUBLIC_SITE_HOST: Apex hostname for the public marketing site in production (for example example.com). Required when WITH_SITE=1. Must differ from PUBLIC_UI_HOST.
SITE_CONTACT_EMAIL, SITE_IMAGE_TAG: Required in production when WITH_SITE=1. The contact mailbox shown on the site, and a pinned image tag (semver or sha-<digest>, never latest). SITE_IMAGE can replace the tag with a full pinned reference.
EMAIL_PROVIDER + EMAIL_FROM: Sender identity and service. Cloudflare is the default; Resend and SendGrid are drop-in alternatives. Set both to avoid bounce errors.
Where the full list lives
Section titled “Where the full list lives”For defaults, type hints, and complete enumeration:
- apps/api:
.env.example - apps/ui:
.env.example - infra:
compose/.env.example
The .env.example files define every variable and its default. When you need to understand a variable’s default or type, start there.
Optional features (toggle by setting vars)
Section titled “Optional features (toggle by setting vars)”- Billing: set
BILLING_ENABLED=true+ Stripe keys - OAuth: set
GOOGLE_OAUTH_CLIENT_ID/CLIENT_SECRETor GitHub/LinkedIn equivalents - Observability: set
WITH_OBSERVABILITY=0to opt out (on by default) - Error tracking: set
WITH_GLITCHTIP=0to opt out (on by default) - Queue dashboard: set
WITH_BULLMQ=1(dev only, no auth) - Email catcher: set
WITH_MAILPIT=1(dev only) - Public marketing site:
WITH_SITEis on in dev and prod. Dev serves it on:7333. Prod needsPUBLIC_SITE_HOST,SITE_CONTACT_EMAILandSITE_IMAGE_TAG. SetWITH_SITE=0to opt out. See Public site
Related
Section titled “Related”- Commands cheatsheet; which scripts to run for setup and day-to-day work
- Profiles & overlays; how STACK= and WITH_* flags compose services
- Deployment; production secrets into 1Password and how to inject them