# .env - DO NOT COMMITDATABASE_URL=postgres://user:pass@localhost/mydbAPI_SECRET_KEY=abc123xyz789STRIPE_SECRET_KEY=sk_test_...
✅ Add to .gitignore:
.env.env.local.env.*.local
2
.env.example - Template (Commit This)
Documents all required variables without real values.
# .env.example - COMMIT THIS# Database connection stringDATABASE_URL=postgres://user:password@host:5432/database# API secret key for JWT signingAPI_SECRET_KEY=your-secret-key-here# Stripe API key (get from dashboard)STRIPE_SECRET_KEY=sk_test_...
✅ Commit to version control
✅ Include comments explaining each variable
✅ Use placeholder values
3
.env.defaults - Application Defaults (Optional)
Safe defaults that work for development.
# .env.defaults - Optional, can commitNODE_ENV=developmentPORT=3000LOG_LEVEL=debugCACHE_ENABLED=false
✅ Can be committed safely
✅ Overridden by .env
✅ No secrets
// ✅ Using || operatorconst port = process.env.PORT || 3000;// ✅ Using ?? (nullish coalescing)const timeout = process.env.TIMEOUT ?? 5000;// ✅ Type conversion with defaultconst maxRetries = parseInt(process.env.MAX_RETRIES || '3', 10);// ❌ Avoid for required variablesconst apiKey = process.env.API_KEY || 'missing';
Python
import os# ✅ Using get() with defaultport = int(os.getenv('PORT', '8000'))log_level = os.getenv('LOG_LEVEL', 'INFO')# ✅ Using environ.get()debug = os.environ.get('DEBUG', 'false').lower() == 'true'# ❌ Avoid for secretsapi_key = os.getenv('API_KEY', 'default')
Go
import "os"// ✅ With fallbackport := os.Getenv("PORT")if port == "" { port = "8080"}// ✅ Using LookupEnv for requiredapiKey, exists := os.LookupEnv("API_KEY")if !exists { log.Fatal("API_KEY is required")}
Rust
use std::env;// ✅ With unwrap_orlet port = env::var("PORT") .unwrap_or_else(|_| String::from("8080"));// ✅ With unwrap_or_defaultlet debug = env::var("DEBUG") .unwrap_or_default();// ✅ Explicit error for requiredlet api_key = env::var("API_KEY") .expect("API_KEY must be set");
# .husky/pre-commit#!/bin/sh. "$(dirname "$0")/_/husky.sh"# Run Envark analysisenvark analyze --fail-on highif [ $? -ne 0 ]; then echo "❌ Envark found high-risk environment variable issues" echo "Fix them or use --no-verify to skip" exit 1fi
// src/app.tsimport env from './config/env';// ✅ Type-safe, validated configconst app = express();app.listen(env.PORT, () => { console.log(`Server running on port ${env.PORT}`);});const db = new Database(env.DATABASE_URL);
.env # Local development (gitignored).env.example # Template (committed).env.development # Development defaults (committed).env.staging # Staging config (gitignored or encrypted).env.production # Production config (never committed).env.test # Test environment (committed)
# ❌ BAD - Never do thisgit add .envgit commit -m "Add environment config"
Why it’s bad:
Exposes secrets in version control
Secrets remain in git history even if removed later
Anyone with repo access gets production credentials
Solution:
# .gitignore.env.env.local.env.*.local
❌ Hardcoding Secrets
// ❌ BADconst API_KEY = 'abc123';const db = new Database('postgres://admin:password@prod-db/app');
Why it’s bad:
Can’t change secrets without code deployment
Secrets in version control
Same secrets across all environments
Solution:
// ✅ GOODconst API_KEY = process.env.API_KEY;const db = new Database(process.env.DATABASE_URL);
❌ Inconsistent Naming
# ❌ BAD - Inconsistent styleDatabase_url=...api-key=...STRIPE_secret=...
Why it’s bad:
Hard to search and group
Looks unprofessional
Confusing for team members
Solution:
# ✅ GOOD - Consistent SCREAMING_SNAKE_CASEDATABASE_URL=...API_KEY=...STRIPE_SECRET_KEY=...
❌ No Defaults for Development
// ❌ BAD - App crashes in developmentconst port = process.env.PORT;app.listen(port); // undefined in dev!
Why it’s bad:
Poor developer experience
Forces every dev to set up .env
Increases onboarding friction
Solution:
// ✅ GOOD - Sensible defaults for devconst port = process.env.PORT || 3000;app.listen(port);
❌ Missing Documentation
# ❌ BAD - No explanationMAX_RETRIES=3CACHE_TTL=300FEATURE_X=true
Why it’s bad:
New team members don’t understand purpose
Hard to know valid values
Difficult to debug configuration issues
Solution:
# ✅ GOOD - Clear documentation# Maximum retry attempts for failed API calls (1-10)MAX_RETRIES=3# Cache time-to-live in seconds (default: 300)CACHE_TTL=300# Enable experimental feature X (requires v2 API)FEATURE_X=true