KwadMarket Docs
Best Practices

Env Validation

Fail fast on missing environment variables — no silent fallbacks

Status (reconciled 2026-07-02)

The web app validates env via lib/env.ts (zod) and the middleware throws without JWT_SECRET in production. The backend still has the dangerous defaults — removing them is plan phase 1, which owns the exact Joi spec.

The cal.diy pattern

apps/api/v2/src/env.ts defines a typed getEnv<K>() helper that throws at startup if a required variable is missing — no silent fallbacks:

type Environment = {
  DATABASE_URL: string;
  JWT_SECRET: string;
  // ...
};

export const getEnv = <K extends keyof Environment>(
  key: K,
  fallback?: Environment[K]
): Environment[K] => {
  const value = process.env[key];
  if (value === undefined) {
    if (fallback !== undefined) return fallback;
    throw new Error(`Missing environment variable: ${key}.`);  // crash fast, loud
  }
  return value as Environment[K];
};

Every key is typed via Environment — a typo on a key name is a compile error.

The KwadMarket problem

apps/back/src/config/env.config.ts — current
JWT_SECRET: Joi.string().default('marketplace-ts-dev-secret'),

A misconfigured production deploy silently uses a known-public JWT secret: all JWTs from that deploy can be forged by anyone who reads the source code. Same class of problem for AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / MAILER_* defaulting to ''.

What to implement

  1. Backend fail-fast — the Joi.when('NODE_ENV', { is: 'production', then: required() }) pattern, specified with grep gates in Security §1. Dev keeps a default; production crashes at boot.
  2. Extend frontend validation — add every NEXT_PUBLIC_* var the app uses to apps/web/lib/env.ts (currently only NEXT_PUBLIC_API_URL); replace direct process.env.NEXT_PUBLIC_* reads with env.*.
  3. Optional, medium term — a typed getEnv<K extends keyof Env>() helper for the backend, eliminating process.env.TYPO_KEY at runtime.

NEXT_PUBLIC_* is build-time inlined; server-only secrets like JWT_SECRET must be validated in a server-only module so they never ship to the client bundle.

On this page