Configuration pattern
How we define and load application configuration so backend and frontends share a single root .env while each app uses a single, consistent set of variable names in code.
Overview
- Single source for local dev: One root
.envat the monorepo root holds all env vars. Backend and frontend apps load from this file so developers edit one place. - Prefixed names in root: In the root
.env, names are prefixed by app. Backend vars useBACKEND_(e.g.BACKEND_PORT,BACKEND_DATABASE_URL; noBACKEND_HOSTin root—host is localhost in dev). Desktop frontend vars useFRONTEND_DESKTOP_for frontend-specific vars (e.g. FRONTEND_DESKTOP_PORT for the dev server port). VITE_BACKEND_URL is not set in root; the desktop Vite config derives it ashttp://localhost:BACKEND_PORTonly when unset (see below). - Strip or map into app names: When loading, the backend strips the
BACKEND_prefix so app code only sees unprefixed names (PORT,DATABASE_URL). The frontend mapsFRONTEND_DESKTOP_toVITE_*so app code only usesimport.meta.env.VITE_BACKEND_URLetc. App code never references the root prefixed names; no dual naming. - Deployment: In deployed environments the platform injects vars directly (backend: e.g.
PORT; frontend: e.g.VITE_BACKEND_URLat build time). Root.envis for local development; optional overrides can live in app-level.envfiles (backend: unprefixed; frontend:VITE_*if desired).
Root .env convention
In root .env | Backend sees | Frontend (desktop) sees |
|---|---|---|
BACKEND_PORT=3000 | process.env.PORT | — |
BACKEND_DATABASE_URL=... | process.env.DATABASE_URL | — |
FRONTEND_DESKTOP_PORT=5173 | — | dev server uses VITE_PORT (server.port); mapped automatically via FRONTEND_DESKTOP_* → VITE_* |
FRONTEND_DESKTOP_* (other vars) | — | import.meta.env.VITE_* |
Dev vs deployed API URL: In dev, VITE_BACKEND_URL is derived by the desktop Vite config as `http://localhost:BACKEND_PORT` only when VITE_BACKEND_URL is undefined (the dev script sets BACKEND_PORT and FRONTEND_DESKTOP_PORT only). In deployed builds the platform sets VITE_BACKEND_URL at build time (e.g. /api for same-origin); the config does not override when already set. No BACKEND_HOST or BACKEND_URL in root.
Root .env.example documents these prefixed vars. Backend and frontend stack rules (configuration) describe how each app loads and exposes them.
Backend
Backend loads root .env then backend/.env via ConfigModule envFilePath (see getEnvFilePaths() in backend config). The validate function (mapAndValidate in env.validation.ts) maps BACKEND_* to unprefixed keys in the config object, then validates with the schema, so app code sees PORT, GOOGLE_CLIENT_ID, etc. See stack rule configuration (backend).
Frontend
Desktop (and any other frontend) sets Vite envDir to the monorepo root. At config time it loads root .env, maps keys starting with FRONTEND_DESKTOP_ to VITE_* (so FRONTEND_DESKTOP_PORT → VITE_PORT for the dev server), and sets VITE_BACKEND_URL to `http://localhost:BACKEND_PORT` only when VITE_BACKEND_URL is undefined (dev default; deployed sets it at build time). Code uses only import.meta.env.VITE_*. See stack rule configuration (frontend) and vite.
Implementation
- Root:
boilerplate/.env.example(and optionally.gitignorefor.env). Backend implements map and validate via ConfigModule envFilePath and validate: mapAndValidate (see backendconfiguration); frontend implements map in desktop Vite config. - Pattern
environmentscan use the same root and prefixes (e.g.BACKEND_/FRONTEND_DESKTOP_for env name if desired) and remains consistent with this pattern.
References
- Stack:
configuration(backend),configuration(frontend),vite,hosting. - Pattern:
environments(environment detection vars).