Environment variables connect your code to the outside world โ€” database URLs, API keys, feature flags. Misconfiguring them is one of the most common causes of production incidents and security breaches. Here's the complete guide to managing them correctly.

The Hierarchy of Config

LayerWhereExampleNever Commit?
Default valuesCode (as fallback)PORT ?? 3000Commit (with safe defaults)
Local dev overrides.env.localDATABASE_URL=localhostYes (.gitignore)
CI/CDPlatform secretsDATABASE_URL=staging-dbYes (platform-managed)
ProductionPlatform secrets / vaultDATABASE_URL=prod-dbYes (platform-managed)
Public configNEXT_PUBLIC_* varsNEXT_PUBLIC_API_URLOK (intentionally public)

Rules for Environment Variables

  1. Never commit secrets to Git. Use .gitignore for .env.local, .env.*.local. If a secret ever hits Git history, rotate it immediately.
  2. Prefix public variables. Next.js uses NEXT_PUBLIC_*. Vite uses VITE_*. This makes it clear what's exposed to the browser.
  3. Validate at startup, not at runtime. Use Zod to validate all env vars when the app starts. If a required var is missing, crash immediately โ€” don't fail mysteriously 3 hours later.
  4. Use different values per environment. Development, staging, and production should have separate database URLs, API keys, and feature flags.

Validation Pattern (Prevent Runtime Surprises)

// env.ts โ€” validate all env vars at startup
import { z } from "zod";

const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  AUTH_SECRET: z.string().min(32),
  STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
  NEXT_PUBLIC_APP_URL: z.string().url().default("http://localhost:3000"),
  FEATURE_NEW_CHECKOUT: z.enum(["true", "false"]).default("false"),
});

export const env = envSchema.parse(process.env);
// If any var is missing or invalid, the app crashes immediately

Managing Secrets Across a Team

ToolBest ForHow It Works
DopplerTeams, automatic syncCentral dashboard โ†’ CLI syncs to local .env. Secrets never on disk.
InfisicalOpen source, self-hostedSelf-hosted Doppler alternative. Inject secrets at build/run time.
1Password CLISmall teams with 1Passwordop run --env-file=.env -- npm run dev. Secret references, not values.
Platform-nativeSimplest, freeVercel/Render/Railway all have secret management built in.

Common Mistakes & Fixes

MistakeFix
Hardcoding API keys in sourceMove to .env.local immediately. Check git history. Rotate if exposed.
NEXT_PUBLIC_* for secretsNEXT_PUBLIC_ vars are bundled into client JS. Anyone can see them. Never put secrets here.
Same API key for dev + prodUse separate keys. Stripe has test mode keys. Dev databases are separate.
.env.example not updatedAdd new vars to .env.example with dummy values. Treat it as documentation.
Secrets in Docker imagesInject at runtime, not at build time. Use docker run -e or Docker secrets.

Bottom line: Validate env vars at startup with Zod. Never put secrets in NEXT_PUBLIC_* or Git. Use Doppler/Infisical for teams, platform-native for side projects. Document every variable in .env.example. See also: Web Security Basics and Error Handling Best Practices.