Configuration
The API reads its configuration once, from environment variables: Worker bindings in staging and production, process.env (plus .env at the repository root) under Node. parseConfig in apps/api/src/config.ts turns them into an AppConfig, and fails closed: anything that would weaken security outside development throws at start-up.
Variables
| Variable | Default | Purpose |
|---|---|---|
ENVIRONMENT | production in parseConfig; development under node.ts | One of development, test, staging, production. Anything else throws. Shown as a badge in the app outside production. |
AUTH_MODE | dev under node.ts; unset in the Worker | dev enables development sign-in (x-dev-user). Only allowed when ENVIRONMENT is development or test. Unset means Cloudflare Access. |
DEV_USER_EMAIL | dev@davies-group.com | The caller when AUTH_MODE=dev and no x-dev-user header is sent. |
ACCESS_TEAM_DOMAIN | — | Cloudflare Access team domain, e.g. davies.cloudflareaccess.com. The JWT issuer and JWKS location. Required unless AUTH_MODE=dev. |
ACCESS_AUD | — | The Access application's AUD tag. Required unless AUTH_MODE=dev. |
ALLOWED_EMAIL_DOMAINS | davies-group.com under node.ts; empty otherwise | Comma-separated domains provisioned automatically on first sign-in. |
BOOTSTRAP_ADMIN_EMAILS | dev@davies-group.com under node.ts; empty otherwise | Comma-separated emails that become administrators when first provisioned (regardless of domain). |
IDENTITY_KEK | Generated into DATA_DIR/dev-identity-kek under node.ts | Base64 of 32 random bytes: the key-encryption key for per-engagement identity keys. Required outside development and test. Never change it once set. |
AI_PROVIDER | gemini when GEMINI_API_KEY is set, otherwise none | gemini, fake or none. fake (the development stand-in) is refused outside development and test; gemini requires GEMINI_API_KEY. |
GEMINI_API_KEY | — | Secret. Enables Gemini. |
GEMINI_MODEL | gemini-3.1-flash-lite | The default model; a prompt version can name another. |
AI_MONTHLY_BUDGET_USD | 50 | AI features pause when estimated month-to-date spend reaches this. |
GEMINI_ALLOWED_MODELS | GEMINI_MODEL | Comma-separated models a prompt version may name; GEMINI_MODEL is always allowed. Other models are refused when a prompt is saved or published. |
AI_ASK_DAILY_LIMIT | 100 | "Ask the data" questions per person per UTC day. |
AI_INPUT_PRICE_PER_MTOK | 0.1 | USD per million input tokens, for the spend estimate. |
AI_OUTPUT_PRICE_PER_MTOK | 0.4 | USD per million output tokens. |
DATABASE_URL | — | Postgres connection string (the owner role). Required in the Worker (secret) and for npm run db:migrate; optional under Node (PGlite otherwise). |
DATA_DIR | .data | Node only: where PGlite and the dev identity key live, relative to the repository root. The e2e suite points it at a temporary folder. |
PORT | 8787 | Node only: the API's port. |
HOST | 127.0.0.1 | Node only: the interface the API listens on. Under development sign-in, requests must still address it as localhost. |
API_PORT | 8787 | The Vite dev server's proxy target (read from the shell environment, not .env). |
Numeric settings fall back to their defaults if missing, blank, negative or not a number.
.env.example.env.example documents most of these for local use. It doesn't yet mention AI_PROVIDER; add AI_PROVIDER=fake to .env to use the development stand-in model.
Where each environment gets its values
| Environment | Where |
|---|---|
Local (npm run dev) | node.ts defaults, overridden by .env at the repository root, overridden by the shell environment. |
| Tests (Vitest) | createHarness() in apps/api/test/harness.ts builds the config directly (ENVIRONMENT=test, dev sign-in, a fixed test KEK). |
| e2e (Playwright) | apps/web/playwright.config.ts starts the Node API with DATA_DIR set to a temporary folder, AI_PROVIDER=fake and an empty GEMINI_API_KEY, on port 8797, and Vite on 5183. |
| Staging and production | Non-secret vars in apps/api/wrangler.jsonc (per environment) or the Cloudflare dashboard; secrets with wrangler secret put. |
wrangler.jsonc sets ENVIRONMENT, GEMINI_MODEL and AI_MONTHLY_BUDGET_USD (50 in production, 10 in staging). ACCESS_TEAM_DOMAIN, ACCESS_AUD, ALLOWED_EMAIL_DOMAINS and BOOTSTRAP_ADMIN_EMAILS are set per environment; DATABASE_URL, IDENTITY_KEK and GEMINI_API_KEY are secrets. See Deployment and operations.
Fail-closed rules
parseConfig throws, and the Worker or server refuses to start, when:
ENVIRONMENTisn't one of the four values;AUTH_MODE=devoutside development or test;- neither
AUTH_MODE=devnor bothACCESS_TEAM_DOMAINandACCESS_AUDare set; AI_PROVIDERis unknown, isfakeoutside development or test, or isgeminiwithout a key;IDENTITY_KEKis missing outside development or test, isn't base64, or doesn't decode to exactly 32 bytes.
The Worker caches the parsed config per env object: a new deployment (and so any changed variable or secret) gets a fresh one.
What /api/me exposes
The app learns what it needs about configuration from GET /api/me:
{
"user": { "id": "…", "email": "dev@davies-group.com", "name": null, "role": "admin" },
"environment": "development",
"aiEnabled": true,
"signIn": { "mode": "dev", "signOutUrl": null }
}
signIn.mode is access (with signOutUrl: "/cdn-cgi/access/logout") or dev. The web client uses it to decide whether to send x-dev-user and whether to reload on an expired session. aiEnabled hides or disables AI controls.