Environment Variable Best Practices: Secrets, Configuration, and Security

Share

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:

.gitignore (essential entries)
# 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:

.env.example (commit this file)
# 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:

Environment validation
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:

FrameworkClient-Exposed PrefixExample
Next.jsNEXT_PUBLIC_NEXT_PUBLIC_API_URL
ViteVITE_VITE_APP_TITLE
Create React AppREACT_APP_REACT_APP_API_URL
NuxtNUXT_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.

What to expose (and what not to)
# 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

Vercel environment variable setup
# 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

Netlify environment configuration
# 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:

SolutionBest For
DopplerUniversal secrets management
AWS Secrets ManagerAWS infrastructure
Google Secret ManagerGCP infrastructure
Vault (HashiCorp)Enterprise, self-hosted
InfisicalOpen 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:

Environment separation
# 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

MistakeImpactPrevention
Committing .env filesSecret exposureAdd to .gitignore immediately
Secrets in NEXT_PUBLIC_Public exposureOnly public config in public vars
Same secrets across envsCross-environment riskUnique secrets per environment
No startup validationRuntime failuresValidate required vars at boot
Hardcoded fallbacksAccidental prod useFail if env var missing

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.

Scan for Exposed Secrets

Check your codebase for accidentally committed secrets.

Start Free Scan
Best Practices

Environment Variable Best Practices: Secrets, Configuration, and Security