Warming up the neural circuits...
By the end of this chapter you will:
A misconfigured app is a vulnerable app. The most common and most damaging config mistakes are: hardcoding secrets in source code, missing required env vars, and using production credentials in development.
Think of config in three layers, from least sensitive to most sensitive:
Tier 1 — Code defaults (safe in all environments)
const PAGE_SIZE = 50; // hardcoded in code — this is fineTier 2 — Environment variables (per-environment, not secret)
NODE_ENV=development
PORT=3002
PGHOST=localhostTier 3 — Secret store (production secrets — AWS Secrets Manager)
JWT_SECRET → fetched at runtime from AWS Secrets Manager, never in .env files in prod
DB_PASSWORD → sameIn development, you use .env files for tiers 2 and 3.
In production, tier 3 comes from AWS Secrets Manager.
.env file setupNever commit .env to Git. Always commit .env.example.
# .env.example — COMMIT THIS (no real values)
NODE_ENV=development
PORT=3002
PGHOST=localhost
PGPORT=5432
PGUSER=youruser
PGPASSWORD=yourpassword
PGDATABASE=quickbite_dev
DB_SSL=false
JWT_SECRET=replace_this_with_minimum_32_character_random_string
# .gitignore — THESE MUST BE HERE
.env
.env.local
.env.productionThe app should refuse to start if any required variable is missing or the wrong type. Clear error at startup beats silent failure at runtime.
// src/config/env.validation.ts
import * as Joi from 'joi';
export const envSchema = Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'staging', '
// app.module.ts
ConfigModule.forRoot({
isGlobal: true,
validationSchema: envSchema,
validationOptions: {
abortEarly: false, // report ALL missing vars at once, not just the first one
},
})If JWT_SECRET is missing when the app starts, you'll see:
ValidationError: "JWT_SECRET" is requiredClear, immediate, and impossible to miss.
Never read environment variables directly with process.env. Always use ConfigService:
// ❌ Wrong — bypasses validation
const secret = process.env.JWT_SECRET;
// ✅ Correct — goes through ConfigModule
@Injectable()
export class AuthService {
constructor(private configService: ConfigService) {}
getSecret() {
return
configService.get() is validated and typed. process.env.JWT_SECRET returns string | undefined — if it's undefined, your code may not fail until a user tries to log in.
// ❌ EXTREMELY DANGEROUS
const jwtSecret = process.env.JWT_SECRET || 'default-secret';
// If JWT_SECRET is missing, every JWT signed with 'default-secret' is valid.
// This is a known string. Anyone can forge tokens.There is no situation where a fallback default for a secret is acceptable. If the secret is missing, the app should crash loudly.
In production, sensitive secrets (DB password, secret) are never stored in files. They're stored in AWS Secrets Manager and fetched at startup via IAM role.
The environment variable AWS_AUTO_SECRET_ENV_VARIABLES controls which secrets to fetch. The app fetches them during bootstrap and merges them into the process environment.
You never need to change this mechanism — it's already set up. But know it exists so you don't accidentally override production secrets by setting them in .env.
| Rule | Why |
|---|---|
Never process.env.X outside ConfigModule | Bypasses validation, returns undefined silently |
Never commit .env | Leaks credentials to everyone with repo access |
Always commit .env.example | New developers can't start without it |
| No fallback to real credentials | ` |
| Rotate secrets quarterly | Limits blast radius of any single leak |
| If a secret leaks to Git: rotate first, clean second | The secret is public the moment it hits the remote |
If JWT_SECRET is missing, crash loudly at startup. If it silently falls back to undefined or a default string, your entire authentication system is bypassed. Fail fast, fail loud.