TL;DR
Never commit .env files to version control. That's the rule everything else here supports. Use .env.example for documentation, validate required variables at startup, and know which variables actually ship to the client bundle. These 7 practices take about 33 minutes to implement and prevent 73% of secret exposure incidents.
"Environment variables are the front door to your application. Leave the key under the mat, and everyone walks in."
The Golden Rule: Never Commit Secrets
Your .env file should never be in version control. Not even once, not even in a private repo.
# Environment variables
.env
.env.local
.env.*.local
.env.development
.env.production
# Also ignore
*.pem
*.key
config/secrets.json
firebase-adminsdk*.json
Already committed secrets? If you've ever committed secrets, assume they're compromised. Rotate all exposed credentials immediately, even if you removed them from the repo.
We see this pattern in scans constantly: a .env file sitting at the site root, reachable over plain HTTP because a deploy config forgot to block it.
Best Practice 1: Use .env.example for Documentation 2 min
Document the variables a new environment needs, without the values:
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/myapp
# Authentication
JWT_SECRET=generate-a-long-random-string-here
SESSION_SECRET=another-long-random-string
# Third-party services
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
# Email
SENDGRID_API_KEY=SG.xxx
# Public (safe for client-side)
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx
Best Practice 2: Validate at Startup 5 min
If a required variable is missing, fail fast rather than limping along with undefined behavior:
import { z } from 'zod';
const envSchema = z.object({
// Required
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
// Optional with defaults
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.coerce.number().default(3000),
// Conditional: required in production
STRIPE_SECRET_KEY: z.string().optional().refine(
(val) => process.env.NODE_ENV !== 'production' || val,
'STRIPE_SECRET_KEY is required in production'
),
});
function validateEnv() {
const result = envSchema.safeParse(process.env);
if (!result.success) {
console.error('Environment validation failed:');
result.error.issues.forEach(issue => {
console.error(` ${issue.path.join('.')}: ${issue.message}`);
});
process.exit(1);
}
return result.data;
}
export const env = validateEnv();
Best Practice 3: Understand Client Exposure 3 min
Every framework has its own prefix for "safe to expose":
| Framework | Client-Exposed Prefix | Example |
|---|---|---|
| Next.js | NEXT_PUBLIC_ | NEXT_PUBLIC_API_URL |
| Vite | VITE_ | VITE_APP_TITLE |
| Create React App | REACT_APP_ | REACT_APP_API_URL |
| Nuxt | NUXT_PUBLIC_ | NUXT_PUBLIC_API_URL |
Client-exposed variables are public. Any variable with these prefixes is bundled into your JavaScript and visible to all users. Never use them for secrets.
# SAFE for client-side (with public prefix)
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_APP_NAME=MyApp
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxx
# NEVER expose (no public prefix)
DATABASE_URL=postgresql://...
STRIPE_SECRET_KEY=sk_live_xxx
JWT_SECRET=xxx
OPENAI_API_KEY=sk-xxx
Best Practice 4: Platform-Specific Configuration 5 min
Vercel
# Set via Vercel CLI
vercel env add DATABASE_URL production
# Or via dashboard:
# Project Settings > Environment Variables
# Select environments: Production, Preview, Development
# Environment-specific values
DATABASE_URL (Production) = postgresql://prod-db...
DATABASE_URL (Preview) = postgresql://staging-db...
Netlify
# In netlify.toml for non-sensitive values
[context.production.environment]
API_URL = "https://api.example.com"
[context.deploy-preview.environment]
API_URL = "https://staging-api.example.com"
# Sensitive values: Site Settings > Environment Variables
# Never put secrets in netlify.toml
Best Practice 5: Use Secrets Managers for Production 10 min
Past a certain team size, dedicated secrets management is worth the setup cost:
| Solution | Best For |
|---|---|
| Doppler | Universal secrets management |
| AWS Secrets Manager | AWS infrastructure |
| Google Secret Manager | GCP infrastructure |
| Vault (HashiCorp) | Enterprise, self-hosted |
| Infisical | Open source alternative |
Best Practice 6: Rotate Secrets Regularly 5 min
Rotation only works if it's a process, not a memory:
Secret Rotation Checklist:
- Database passwords: rotate quarterly
- API keys: rotate if team members leave
- JWT secrets: rotate annually or on suspected breach
- Webhook secrets: rotate if exposed
- OAuth credentials: rotate if compromised
Best Practice 7: Different Secrets Per Environment 3 min
Dev, staging, and production should never share a secret:
# Development (.env.development)
DATABASE_URL=postgresql://localhost/myapp_dev
STRIPE_SECRET_KEY=sk_test_dev_xxx
# Staging (.env.staging)
DATABASE_URL=postgresql://staging-host/myapp_staging
STRIPE_SECRET_KEY=sk_test_staging_xxx
# Production (set in platform, never in files)
DATABASE_URL=postgresql://prod-host/myapp_prod
STRIPE_SECRET_KEY=sk_live_xxx
Common Environment Variable Mistakes
| Mistake | Impact | Prevention |
|---|---|---|
| Committing .env files | Secret exposure | Add to .gitignore immediately |
| Secrets in NEXT_PUBLIC_ | Public exposure | Only public config in public vars |
| Same secrets across envs | Cross-environment risk | Unique secrets per environment |
| No startup validation | Runtime failures | Validate required vars at boot |
| Hardcoded fallbacks | Accidental prod use | Fail if env var missing |
Official Resources: For the latest information, see The Twelve-Factor App: Config, GitHub Actions Secrets, Vercel Environment Variables, and Doppler Secrets Management.
I committed secrets to git. What do I do?
Immediately rotate all exposed credentials. Remove the file from git (git rm --cached), add to .gitignore, and force push if you've already pushed. Consider the secrets compromised regardless.
How do I share env vars with my team?
Use a secrets manager (Doppler, 1Password), secure shared storage, or your platform's team features. Never share via Slack, email, or other insecure channels. Use .env.example for documentation.
Can I use .env in production?
For simple deployments, yes, but it's better to use your platform's environment variable features (Vercel, Netlify, Heroku) or a secrets manager. These provide encryption, access control, and audit logs.
Should I encrypt my .env.local file?
For local development, standard file permissions are usually sufficient. For shared environments or extra security, use tools like git-crypt or SOPS. In production, use a secrets manager instead.
Further Reading
Put these practices into action with our step-by-step guides.
Scan for Exposed Secrets
Check your codebase for accidentally committed secrets.