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,
.envfor 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 | .env | JSON config |
|---|---|---|
| Flat key/value | ✓ | ✓ |
| Nested objects | hacky (prefixes) | ✓ |
| Arrays | comma-separated strings | ✓ |
| Types beyond string | coerce manually | ✓ |
| Comments | ✓ | ✗ (use JSON5 / YAML) |
| Hot-reload in prod | no (host injects) | possible |
| Host/CI support | universal | you ship the file |
| Secret-safety | git-ignored by default | easy to commit by accident |
| Human-editable | very | yes, 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.