TL;DR
The #1 environment variables best practice is never committing .env files to version control. Use .env.example for documentation, validate required variables at startup, and understand which variables are exposed to the client. 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:
# 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 have ever committed secrets, assume they are compromised. Rotate all exposed credentials immediately, even if you removed them from the repo.
Best Practice 1: Use .env.example for Documentation 2 min
Create a template file that documents required variables:
# 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
Fail fast if required variables are missing:
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
Different frameworks expose variables differently:
| 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
For production, consider dedicated secrets management:
| 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
Have a process for rotating credentials:
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
Never share secrets between environments:
# 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 have 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 is 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.