Skip to main content

Concern

Application configuration and environment-specific settings.

Technology

@nestjs/config (NestJS ConfigModule), zod (schema validation), environment variables

Documentation

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 lowercase development, acceptance, production (shared enum). Run-time. Read by getEnvironment() only (no NODE_ENV fallback). Set as the first env in DO app specs (value acceptance or production); see pattern environments.
  • PORT — HTTP port (default 3000). Run-time; in deployment the platform often sets it (e.g. App Platform injects PORT from http_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 development locally, production in deployed; run-time. Not used for environment detection (use ENVIRONMENT and getEnvironment(); see pattern environments).
  • 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 rule hosting.
  • MAILING_TYPE, MAILGUN_* — Mailing implementation and Mailgun config; see stack rule mailing. MAILING_TYPE defaults to local; when ENVIRONMENT is acceptance or production, a refinement requires MAILING_TYPElocal (e.g. set to mailgun and 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 .env files, 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 .env are 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_IDGOOGLE_CLIENT_ID when 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 be local when ENVIRONMENT is acceptance or production; see mailing), writes validated values back to process.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.