At scale, environment variables become an undocumented API between teams. New hires grep for process.env across forty packages and guess. Structure is documentation + naming + boundaries — not a fancier .env parser.


1. Principles (12-factor aligned)

The Twelve-Factor config factor separates code (same across deploys) from config (varies per environment). In large codebases:

  • Names are stable and namespaced.
  • Defaults for non-secret config can live in code or small committed files.
  • Secrets are never committed — only templates.

2. Naming conventions

Prefix by subsystem

WEB_PUBLIC_SITE_URL          # safe for client if truly public
API_DATABASE_URL             # server only
WORKER_REDIS_URL
SHARED_OTEL_EXPORTER_OTLP_ENDPOINT

Explicit environment scope (optional suffix)

PAYMENTS_STRIPE_WEBHOOK_SECRET_DEV
PAYMENTS_STRIPE_WEBHOOK_SECRET_STAGING

Better yet: separate deploys with separate secret stores so names do not need _PROD in every key.


3. Monorepo layout

Per-app .env.example

apps/
  web/.env.example
  api/.env.example
  worker/.env.example

Root README links to each. Avoid a root .env that becomes a god object.

Shared packages

Never put secrets in packages/ui or packages/types. If a package needs config, accept parameters from the app that owns secrets.


4. Microservices (polyrepo)

Standardize cross-cutting names across repos:

SERVICE_NAME
OTEL_SERVICE_NAME
LOG_LEVEL

Document service-specific keys in each repo’s docs/env.md.


5. Infrastructure-as-code vs runtime

MaterialBelongs in
Terraform variables (non-secret)*.tfvars.example committed
Terraform secretsRemote backend / CI vars / Vault — not committed
App runtimeInjected by platform at container start

Do not duplicate the same secret in both Terraform state and also every developer’s .env unless you have a migration story.


6. Discovery tooling (optional but valuable)

  • Zod / envalid / language equivalents: validate required env at startup with a single error message listing missing keys.
  • Single config.ts per app that reads process.env once — stop scattering raw process.env.FOO across 200 files.

Example (TypeScript sketch):

// apps/api/src/config.ts
function required(name: string): string {
  const v = process.env[name];
  if (!v) throw new Error(`Missing required env: ${name}`);
  return v;
}

export const config = {
  databaseUrl: required("API_DATABASE_URL"),
  port: Number(process.env.PORT ?? "3000"),
};

7. Human documentation

Maintain docs/environment.md per service:

  • Table: Name | Required? | Example (fake) | Owner team | How to obtain.

8. Where PassStore fits

Developers still need one place on macOS for their dev copies — PassStore workspaces mirror per-service groups without duplicating prod material on disk. Pair with organize secrets across multiple projects.