# Configuration Management

## 1. What Counts as Configuration

**Configuration** is everything that differs between deployments of the *same build*: database URLs, API keys, feature flags, log levels, ports, timeouts, third-party endpoints. Code is the same in dev, staging and production; config is not.

The twelve-factor rule: **store config in the environment**, not in the code. A useful test — could you open-source the repository right now without leaking any credentials? If not, config is living in code.

## 2. Sources and Precedence

Most apps merge several sources, later ones overriding earlier ones:

```
defaults in code            PORT=3000, LOG_LEVEL=info
  ← config file             config/production.json
  ← .env file (local dev)   DATABASE_URL=postgres://localhost/dev
  ← environment variables   set by the platform (Docker, k8s, systemd)
  ← CLI flags               --port 8080
```

| Source | Good for | Avoid for |
|---|---|---|
| code defaults | safe non-secret defaults | anything environment-specific |
| config files | structured, non-secret settings checked into git | secrets |
| `.env` | local development convenience | production (never commit it) |
| environment variables | everything per-deployment, incl. secrets injected by the platform | huge structured config |
| secret manager (Vault, AWS Secrets Manager, k8s Secrets) | credentials, rotation, audit | values that change per request |

## 3. Validate Config at Startup — Fail Fast

Environment variables are untyped strings. Parse and validate them **once**, at boot, into a typed object:

```ts
import { z } from "zod";

const ConfigSchema = z.object({
  NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
  PORT: z.coerce.number().int().min(1).max(65535).default(3000),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
  FEATURE_NEW_CHECKOUT: z.enum(["true", "false"]).default("false").transform((v) => v === "true"),
});

export type Config = z.infer<typeof ConfigSchema>;

export function loadConfig(env = process.env): Config {
  const result = ConfigSchema.safeParse(env);
  if (!result.success) {
    // print WHICH keys are wrong — never their values (they may be secrets)
    console.error("Invalid configuration:", Object.keys(result.error.flatten().fieldErrors).join(", "));
    process.exit(1);
  }
  return Object.freeze(result.data);
}
```

A missing `DATABASE_URL` should crash the deploy in second one — not surface as a confusing `ECONNREFUSED` on the first request an hour later.

## 4. Inject Config, Don't Read `process.env` Everywhere

```ts
// ❌ scattered: hard to test, typos become silent undefined
const timeout = Number(process.env.PAYMENT_TIMEOUT_MS);

// ✅ one typed object, passed in
class PaymentClient {
  constructor(private readonly cfg: Pick<Config, "PAYMENT_URL" | "PAYMENT_TIMEOUT_MS">) {}
}
```

NestJS's `ConfigModule` does exactly this: `ConfigModule.forRoot({ validate })` validates at boot, and `ConfigService` is injected wherever a value is needed (`09-dependency_injection/`). Tests construct the config object directly instead of mutating `process.env`.

## 5. Secrets

- **Never commit secrets** — add `.env` to `.gitignore`, commit a `.env.example` with placeholder values.
- **Inject at run time** from a secret manager or orchestrator (k8s Secret → env var or mounted file).
- **Rotate** without redeploying code: read secrets from a mounted file that the platform updates, or re-fetch from the secret manager periodically.
- **Don't log config objects** wholesale; redact (`13-logging/` section 5).
- Prefer short-lived credentials (IAM roles, workload identity) over long-lived static keys where the platform supports them.

## 6. Feature Flags

Feature flags are runtime config that turns code paths on/off without a deploy:

```ts
if (config.FEATURE_NEW_CHECKOUT) return newCheckout(cart);
return legacyCheckout(cart);
```

Env-var flags need a restart to change. Dedicated flag services (LaunchDarkly, Unleash, OpenFeature providers) evaluate per request with targeting rules (percentage rollouts, specific users), which enables canary releases (`devops-cloud/cicd/02-deployment_strategies`). Remove flags once a rollout completes — stale flags are tech debt.

## Under the Hood: What `process.env` Really Is

When the OS starts a process (`execve`), it copies the parent's **environment block** — a flat array of `KEY=value` C strings — into the new process's memory. Node exposes it as `process.env`, an object whose property access is intercepted so reads and writes go to the real environment:

- Every value is a **string**. Assigning `process.env.PORT = 8080` stores `"8080"`; assigning `undefined` stores the string `"undefined"` (use `delete`).
- On Windows keys are case-insensitive; on Linux/macOS they're case-sensitive.
- The environment is **copied at process start**. Changing a variable in your shell after the app started does nothing; a child process inherits whatever the parent has at spawn time.
- Anyone who can inspect the process can usually read it (`/proc/<pid>/environ` on Linux for the same user, crash dumps, `docker inspect` showing container env) — env vars are convenient for secrets, but not a vault. Mounted secret files with tight permissions reduce exposure.

`dotenv` simply reads `.env`, parses `KEY=value` lines and assigns them into `process.env` **only if not already set** — which is why real environment variables override `.env` values.

![Defaults, config files, a .env file and real environment variables are layered with later sources overriding earlier ones, then validated once at startup into a frozen typed config object injected into services](diagrams/01-config-precedence.svg)

## Common Pitfalls

1. **`Number(process.env.X)` without validation** — a typo yields `NaN` silently.
2. **`process.env.FLAG === true`** — always false; env values are strings.
3. **Committing `.env`** or secrets in config files.
4. **Reading config lazily deep in the code** — misconfiguration discovered at the worst possible time.
5. **Logging the whole config** on startup — leaks secrets into the log system.
6. **Different config *shape* per environment** — environments should differ in values, not in which keys exist.

## Quick Reference

| Need | How |
|---|---|
| per-deploy values | env vars |
| local dev | `.env` + `.env.example` (committed) |
| typed + validated | schema parse at boot, freeze, inject |
| secrets in prod | secret manager / orchestrator secrets |
| booleans from env | `v === "true"` (explicit) |
| NestJS | `ConfigModule.forRoot({ validate })` + `ConfigService` |
| runtime toggles | feature-flag service |
