Skip to main content

Configuration

Status: Scaffolded 2026-09-24

All configuration is environment variables, read once by src/config/env.ts, validated with zod, and exported as a typed Env object. Nothing else in the codebase touches process.env. .env.example carries every name with one comment and no values; the real file lives outside the checkout on the host and in .env (gitignored) locally.

Fail fast​

loadEnv() runs envSchema.safeParse(process.env) and throws an EnvError listing every problem at once:

Invalid environment:
MONGODB_URI: MONGODB_URI is required
DM_ENCRYPTION_KEY: must be 32 bytes, base64 encoded

server.ts calls it before connecting to anything, so a misconfigured process exits in the first second with a readable message and PM2 records the restart. The schema also has cross-field rules: EMAIL_TRANSPORT=smtp requires the SMTP variables; NODE_ENV=production requires DM_ENCRYPTION_KEY, CORS_ORIGINS, and EMAIL_TRANSPORT=smtp. Development can run with a near-empty file.

Variables​

Shapes only. Values are never written anywhere public.

NamePurposeShape and default
NODE_ENVmodedevelopment, test, production
PORTlisten port on loopback behind nginxinteger
LOG_LEVELpino levelfatal to trace, default info
TRUST_PROXYhops to trust for the client addressinteger, 0 locally, 1 behind nginx
CORS_ORIGINSbrowser origins allowed to call the APIcomma-separated URLs, required in production
MONGODB_URIAtlas connection stringmongodb+srv://..., required
MONGODB_DB_NAMEdatabase name overrideoptional
SESSION_TTL_DAYSsession lifetimeinteger, default 30
MAGIC_LINK_TTL_MINUTESmagic link lifetimeinteger, default 15
MAGIC_LINK_BASE_URLthe web page the email links toURL
RATE_LIMIT_AUTH_MAXrequests per window on /api/auth/*integer
RATE_LIMIT_MAGIC_LINK_MAXper window on magic-link issueinteger, lower
RATE_LIMIT_COMPANION_MAXper window on the companioninteger
EMAIL_TRANSPORThow mail is sentlog (print to the log, dev) or smtp
EMAIL_FROMsenderName <address>
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSSES SMTP credentialsrequired when transport is smtp
AWS_REGIONfor S3 presigningdefault us-east-1
S3_MEDIA_BUCKETimage bucketbucket name
S3_PRESIGN_TTL_SECONDSpresigned PUT lifetime30 to 3600, default 300
BUNNY_STREAM_LIBRARY_ID, BUNNY_STREAM_API_KEYvideo uploadsoptional until video ships
ANTHROPIC_API_KEYcompanionoptional; the route answers 503 without it
COMPANION_MODELmodel idstring, default claude-opus-5; configuration, never a literal in code
COMPANION_MAX_TOKENScompletion capinteger
COMPANION_HISTORY_LIMITmessages kept per conversationinteger, default 20
DM_ENCRYPTION_KEYAES-256-GCM key for messages at rest32 bytes, base64; required in production
DM_KEY_IDstored with each message for rotationstring, default v1
DM_RETENTION_DAYSmessage TTLinteger, default 90
APPLE_CLIENT_IDSign in with Apple audienceoptional
GOOGLE_CLIENT_IDSGoogle Sign-In audiencescomma-separated, optional
OVERPASS_URLOverpass mirror for the OSM seedURL
STATS_CACHE_TTL_MS/api/stats cacheinteger, default 5 minutes
REMINDER_TICK_MSreminder worker intervalinteger

Local development​

NODE_ENV=development
MONGODB_URI=<a local MongoDB or a dev Atlas cluster>
EMAIL_TRANSPORT=log
CORS_ORIGINS=<the web app's local dev origin>

EMAIL_TRANSPORT=log prints the magic link to the terminal instead of sending it, so login works with no SES account. The companion, S3, Bunny, and the social providers are all optional locally; their routes return 503 { code: 'not_configured' } rather than crashing.

Rotation​

DM_ENCRYPTION_KEY is rotated by adding the new key under a new DM_KEY_ID, decrypting with whichever id a message carries, and re-encrypting on read or in a worker. The move from an env key to KMS is ADR 0011's follow-up. Session tokens need no rotation: they are random per session and only their hash is stored.