compareupdated 2026-04-17

.env vs JSON config: when to use which

When .env wins, when JSON wins, and why most teams end up with a hybrid. Comparison of parsing cost, type safety, ergonomics, and CI/CD friendliness.

TL;DR

  • .env wins for secrets, per-environment values, and CI/CD friendliness.
  • JSON wins for structured, nested, non-secret config (feature flags, UI themes, routing tables).
  • Most production teams use both: JSON for structure, .env for secrets.

Why .env exists

.env files fit the Twelve-Factor model: config is injected by the environment (host, container, CI runner), not baked into the build. Every cloud provider supports it natively — set a key in Vercel's dashboard and it shows up as process.env.KEY at runtime. No deploy needed.

Why JSON shows up anyway

JSON handles things .env can't: nested objects, arrays, and rich types. A feature-flag file, a tenant routing table, or a UI theme config is miserable to express as FEATURE_FLAGS_DARK_MODE=true,FEATURE_FLAGS_BETA=false.

Feature comparison

Feature.envJSON config
Flat key/value
Nested objectshacky (prefixes)
Arrayscomma-separated strings
Types beyond stringcoerce manually
Comments✗ (use JSON5 / YAML)
Hot-reload in prodno (host injects)possible
Host/CI supportuniversalyou ship the file
Secret-safetygit-ignored by defaulteasy to commit by accident
Human-editableveryyes, but noisier

When to use each

Use .env for

  • Database URLs and connection strings
  • API keys and secrets
  • Per-environment values (NODE_ENV, log level, host names)
  • Feature flags that change per deploy

Use JSON (or YAML/TOML) for

  • Multi-tenant config (lookup tables, routing maps)
  • Structured feature flags with nested metadata
  • i18n / localization bundles
  • Static config that ships with the build and doesn't change per env

The hybrid pattern

Most real-world apps do this:

// config/app.json (committed)
{
  "appName": "MyApp",
  "features": { "darkMode": true },
  "rateLimits": { "default": 60, "auth": 10 }
}

// .env (not committed)
DATABASE_URL=postgres://...
STRIPE_SECRET_KEY=sk_live_...

Then in code: structured config comes from the JSON import; secrets and environment-specific values come from process.env.

Conversion between the two

When migrating from one to the other, the conversion is usually mechanical. Use the .env → JSON or JSON → .env tool — both run in-browser and handle type coercion, nested flattening, and inline comments.

Our recommendation

Pick .env by default. Add JSON only when you hit something that .env can't express cleanly — nested structures, arrays, or static non-secret data that benefits from version control.

Related guides

Related tools