Concern
Application configuration and environment-specific settings.
Technology
@nestjs/config (NestJS ConfigModule), zod (schema validation), environment variables
Documentation
- Website: https://docs.nestjs.com/techniques/configuration
- Repository: https://github.com/nestjs/config
- Getting started: https://docs.nestjs.com/techniques/configuration
- Schema validation: Custom validate function
Integration
framework / NestJS
ConfigModule is registered in AppModule with a custom validate function. Typed config is exposed via AppConfigService (custom wrapper). Injected in service-layer and used at runtime.
validation / zod
Configuration is validated at bootstrap using a Zod schema. The validate function parses process.env with the schema; invalid or missing required values prevent the app from starting. See stack rule validation.
Configuration
ConfigModule is global (isGlobal: true). Env is loaded before Nest bootstrap (see Implementation); ConfigModule uses ignoreEnvFile: true and a custom validate function so the same env (root + backend .env, BACKEND_ stripped) is validated with Zod. Services inject AppConfigService for typed config access (see Implementation).
Environment variables
- ENVIRONMENT — Required with default
development; values lowercasedevelopment,acceptance,production(shared enum). Run-time. Read bygetEnvironment()only (no NODE_ENV fallback). Set as the first env in DO app specs (valueacceptanceorproduction); see patternenvironments. - PORT — HTTP port (default
3000). Run-time; in deployment the platform often sets it (e.g. App Platform injectsPORTfromhttp_port). When running via root pnpm dev, PORT is set automatically to an available port (prefer 3000) by scripts/dev.js; see stack rule development. - NODE_ENV — Often
developmentlocally,productionin deployed; run-time. Not used for environment detection (use ENVIRONMENT andgetEnvironment(); see patternenvironments). - DATABASE_URL — Optional; when using
orm/Prisma; run-time. - JWT_ACCESS_TOKEN_SECRET, JWT_ACCESS_TOKEN_EXPIRATION — Optional; expiration in seconds (default 86400 = 24h). Defaults in schema for local dev (see capability
auth). - JWT_REFRESH_TOKEN_SECRET, JWT_REFRESH_TOKEN_EXPIRATION — Optional; expiration in seconds (default 604800 = 7d). Defaults in schema for local dev (see capability
auth). - JWT_COOKIE_NAME — Cookie name for access token (default
user_token). Run-time. - GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET — For Google SSO (default empty). Run-time.
- MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET — For Microsoft SSO (default empty). Run-time.
- HOST — Backend host for deriving BACKEND_URL (default
localhost). Run-time. Ignored when APP_URL is set. - FRONTEND_HOST, FRONTEND_PORT — Frontend origin for deriving FRONTEND_URL (defaults
localhost,5173). Run-time. Ignored when APP_URL is set. - APP_URL — Optional. When set (e.g. from GitLab CI in deployment), BACKEND_URL and FRONTEND_URL are derived from it (BACKEND_URL = APP_URL +
/api, FRONTEND_URL = APP_URL) so the app uses the configured domain. Used for same-origin deployment; see stack rulehosting. - MAILING_TYPE, MAILGUN_* — Mailing implementation and Mailgun config; see stack rule
mailing. MAILING_TYPE defaults tolocal; when ENVIRONMENT is acceptance or production, a refinement requires MAILING_TYPE ≠local(e.g. set tomailgunand configure MAILGUN_API_KEY, MAILGUN_DOMAIN, etc.).
Dev uses a root .env at monorepo root (see pattern configuration) and optionally backend/.env for overrides; deployed uses platform-provided env at run-time.
Implementation
Env loading and mapping (ConfigModule): The backend uses @nestjs/config. In AppModule, ConfigModule.forRoot() is called with envFilePath and validate: mapAndValidate. No env loading or prefix stripping is done in main.ts; main.ts only bootstraps the app, cookie parser, CORS, and listen.
- envFilePath — Paths to
.envfiles, from getEnvFilePaths() in backend/src/config/env-paths.ts (e.g.[path.resolve(process.cwd(), '..', '.env'), path.resolve(process.cwd(), '.env')]so root then backend.envare loaded in order; Nest loads them and later files override earlier). - isGlobal: true — ConfigService is available in all modules without importing ConfigModule.
- validate: mapAndValidate — A function in backend/src/config/env.validation.ts that receives the config from the env files. It first maps BACKEND_* keys to unprefixed keys in place (e.g.
BACKEND_GOOGLE_CLIENT_ID→GOOGLE_CLIENT_IDwhen the unprefixed key is missing or empty), then parses with appConfigSchema.loose().refine(...).refine(...).parse(config) (first refinement: APP_URL required when ENVIRONMENT is acceptance or production; second refinement: MAILING_TYPE must not belocalwhen ENVIRONMENT is acceptance or production; seemailing), writes validated values back toprocess.env, and returns the parsed result. A failed parse throws and prevents the app from loading.
The backend ships with backend/.env.example that points to root .env.example for shared vars and documents unprefixed overrides.
Schema and mapAndValidate: Define the schema in backend/src/config/env.schema.ts (e.g. appConfigSchema = z.object({...}) only; mapAndValidate in backend/src/config/env.validation.ts maps BACKEND_* to unprefixed keys in the config, then calls appConfigSchema.loose().refine(...).parse(config) so loose/refinement are applied at parse; schema keys: PORT, HOST, FRONTEND_HOST, FRONTEND_PORT, ENVIRONMENT as z.nativeEnum(Environment).default(Environment.Development), NODE_ENV, DATABASE_URL, JWT keys, JWT_COOKIE_NAME, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET; use z.coerce.number() for PORT, FRONTEND_PORT, and JWT expiration vars—defaults 86400 and 604800 seconds; z.string().optional() or defaults for the rest). Auth (JWT, cookie, Google SSO, Microsoft SSO) is merged here; auth code uses AppConfigService.get(key) directly (e.g. JWT_ACCESS_TOKEN_SECRET, FRONTEND_URL). AppConfig extends the schema shape with derived BACKEND_URL and FRONTEND_URL (set in AppConfigService.buildConfig() from HOST/PORT and FRONTEND_HOST/FRONTEND_PORT). mapAndValidate writes the parsed result to process.env; a failed parse throws and prevents the app from loading.
Environment detection: The backend uses pattern environments: shared enum and getEnvironment() in backend/src/config/environment.ts, reading process.env.ENVIRONMENT (no NODE_ENV fallback); when unset, return Environment.Development. CORS and other code use getEnvironment().
Typed access (schema-derived type): The schema infers ValidatedEnv; AppConfig (known keys only) is exported from backend/src/config/env.schema.ts. Services inject AppConfigService for typed config.
AppConfigService: A custom injectable in backend/src/config/app-config.service.ts that wraps ConfigService<AppConfig>, builds AppConfig at construction by iterating over appConfigSchema.shape and then setting BACKEND_URL = http://${HOST}:${PORT} and FRONTEND_URL = http://${FRONTEND_HOST}:${FRONTEND_PORT}, and exposes:
- getConfig(): AppConfig — full validated config object.
- get(key): AppConfig[K] — single-key access. Auth module and strategies inject AppConfigService and use get() for the keys they need (e.g.
JWT_COOKIE_NAME,JWT_ACCESS_TOKEN_SECRET,FRONTEND_URL,BACKEND_URL).
AppConfigModule (backend/src/config/app-config.module.ts) is @Global(), provides and exports AppConfigService. AppModule imports AppConfigModule once; any service can then inject AppConfigService without importing the module.