Initial commit
This commit is contained in:
635
skills/env-config-validator/references/env_best_practices.md
Normal file
635
skills/env-config-validator/references/env_best_practices.md
Normal file
@@ -0,0 +1,635 @@
|
||||
# Environment Variable Best Practices
|
||||
|
||||
Comprehensive guide to managing environment variables in Next.js applications.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Security Best Practices](#security-best-practices)
|
||||
2. [Naming Conventions](#naming-conventions)
|
||||
3. [Scoping Rules](#scoping-rules)
|
||||
4. [Common Patterns](#common-patterns)
|
||||
5. [Environment-Specific Configuration](#environment-specific-configuration)
|
||||
6. [Secret Rotation](#secret-rotation)
|
||||
7. [Testing Strategies](#testing-strategies)
|
||||
8. [Deployment Checklist](#deployment-checklist)
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### 1. Never Commit Secrets
|
||||
|
||||
**Rule**: Never commit `.env.local` or `.env.production` files
|
||||
|
||||
**Setup `.gitignore`**:
|
||||
```
|
||||
# Environment files with secrets
|
||||
.env*.local
|
||||
.env.production
|
||||
.env.staging
|
||||
|
||||
# Only commit the template
|
||||
!.env.example
|
||||
```
|
||||
|
||||
### 2. Use Strong, Random Secrets
|
||||
|
||||
**Requirements for secrets**:
|
||||
- Minimum 32 characters
|
||||
- Cryptographically random
|
||||
- High entropy (mix of characters, numbers, symbols)
|
||||
|
||||
**Generate strong secrets**:
|
||||
```bash
|
||||
# Node.js (recommended for JWT/session secrets)
|
||||
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||
|
||||
# OpenSSL
|
||||
openssl rand -hex 32
|
||||
|
||||
# Python
|
||||
python -c "import secrets; print(secrets.token_hex(32))"
|
||||
```
|
||||
|
||||
### 3. Scope Variables Correctly
|
||||
|
||||
**Public variables** (`NEXT_PUBLIC_*`):
|
||||
- [OK] API endpoints
|
||||
- [OK] Feature flags
|
||||
- [OK] Client-side config
|
||||
- [X] API keys
|
||||
- [X] Database credentials
|
||||
- [X] Any secrets
|
||||
|
||||
**Private variables** (no prefix):
|
||||
- [OK] Database URLs
|
||||
- [OK] API secrets
|
||||
- [OK] JWT/session secrets
|
||||
- [OK] Third-party service credentials
|
||||
|
||||
### 4. Validate on Startup
|
||||
|
||||
**Validate environment variables when app starts**:
|
||||
|
||||
```typescript
|
||||
// lib/env.ts
|
||||
import { z } from 'zod';
|
||||
|
||||
const envSchema = z.object({
|
||||
DATABASE_URL: z.string().url(),
|
||||
JWT_SECRET: z.string().min(32),
|
||||
NEXTAUTH_SECRET: z.string().min(32),
|
||||
NODE_ENV: z.enum(['development', 'staging', 'production']),
|
||||
NEXT_PUBLIC_API_URL: z.string().url(),
|
||||
});
|
||||
|
||||
export const env = envSchema.parse(process.env);
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Fail fast on missing/invalid variables
|
||||
- Type-safe access to env vars
|
||||
- Clear documentation of required variables
|
||||
|
||||
### 5. Use Secret Management Tools
|
||||
|
||||
**For production environments**:
|
||||
|
||||
- **Vercel**: Use Environment Variables UI (automatic encryption)
|
||||
- **AWS**: AWS Secrets Manager or Systems Manager Parameter Store
|
||||
- **GCP**: Secret Manager
|
||||
- **Azure**: Key Vault
|
||||
- **HashiCorp Vault**: Enterprise secret management
|
||||
- **Doppler**: Universal secrets manager
|
||||
|
||||
**Benefits**:
|
||||
- Encryption at rest
|
||||
- Access control and auditing
|
||||
- Automatic rotation
|
||||
- Version history
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
### Standard Format
|
||||
|
||||
Use `SCREAMING_SNAKE_CASE` for all environment variables:
|
||||
|
||||
```bash
|
||||
# [OK] CORRECT
|
||||
DATABASE_URL="..."
|
||||
JWT_SECRET="..."
|
||||
NEXT_PUBLIC_API_URL="..."
|
||||
|
||||
# [X] WRONG
|
||||
databaseUrl="..."
|
||||
jwt-secret="..."
|
||||
NextPublicApiUrl="..."
|
||||
```
|
||||
|
||||
### Prefixes
|
||||
|
||||
**Next.js public variables**:
|
||||
```bash
|
||||
NEXT_PUBLIC_API_URL="https://api.example.com"
|
||||
NEXT_PUBLIC_APP_NAME="My App"
|
||||
NEXT_PUBLIC_GOOGLE_MAPS_KEY="..." # Only if client-side usage
|
||||
```
|
||||
|
||||
**Service-specific prefixes** (optional but recommended):
|
||||
```bash
|
||||
# Database
|
||||
DATABASE_URL="..."
|
||||
DATABASE_POOL_SIZE="..."
|
||||
|
||||
# Stripe
|
||||
STRIPE_PUBLIC_KEY="..."
|
||||
STRIPE_SECRET_KEY="..."
|
||||
STRIPE_WEBHOOK_SECRET="..."
|
||||
|
||||
# Email
|
||||
SMTP_HOST="..."
|
||||
SMTP_PORT="..."
|
||||
SMTP_USER="..."
|
||||
SMTP_PASSWORD="..."
|
||||
```
|
||||
|
||||
### Avoid Redundancy
|
||||
|
||||
```bash
|
||||
# [OK] GOOD - clear and concise
|
||||
DATABASE_URL="..."
|
||||
JWT_SECRET="..."
|
||||
|
||||
# [X] BAD - redundant
|
||||
DATABASE_CONNECTION_URL="..." # "CONNECTION" is redundant with "URL"
|
||||
JWT_SECRET_KEY="..." # "KEY" is redundant with "SECRET"
|
||||
```
|
||||
|
||||
## Scoping Rules
|
||||
|
||||
### Public Variables (`NEXT_PUBLIC_*`)
|
||||
|
||||
**When to use**:
|
||||
- Client-side API endpoints
|
||||
- Feature flags that affect UI
|
||||
- Public configuration (app name, version)
|
||||
- Client-side analytics IDs
|
||||
- Map API keys (if client-side only)
|
||||
|
||||
**Example**:
|
||||
```bash
|
||||
NEXT_PUBLIC_API_URL="https://api.example.com"
|
||||
NEXT_PUBLIC_ANALYTICS_ID="G-XXXXXXXXXX"
|
||||
NEXT_PUBLIC_FEATURE_NEW_UI="true"
|
||||
```
|
||||
|
||||
**Access in code**:
|
||||
```typescript
|
||||
// Available in both server and client components
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||
```
|
||||
|
||||
### Private Variables (no prefix)
|
||||
|
||||
**When to use**:
|
||||
- Database credentials
|
||||
- API secrets
|
||||
- JWT/session secrets
|
||||
- Third-party service credentials
|
||||
- Server-side API keys
|
||||
|
||||
**Example**:
|
||||
```bash
|
||||
DATABASE_URL="postgresql://..."
|
||||
JWT_SECRET="..."
|
||||
STRIPE_SECRET_KEY="sk_live_..."
|
||||
OPENAI_API_KEY="sk-..."
|
||||
```
|
||||
|
||||
**Access in code**:
|
||||
```typescript
|
||||
// Only available in server components and API routes
|
||||
const dbUrl = process.env.DATABASE_URL; // Server-side only
|
||||
```
|
||||
|
||||
### Mixed Scenarios
|
||||
|
||||
**Stripe example** (public + private keys):
|
||||
```bash
|
||||
# Public key - client-side checkout
|
||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY="pk_live_..."
|
||||
|
||||
# Secret key - server-side payments
|
||||
STRIPE_SECRET_KEY="sk_live_..."
|
||||
|
||||
# Webhook secret - server-side webhooks
|
||||
STRIPE_WEBHOOK_SECRET="whsec_..."
|
||||
```
|
||||
|
||||
**Map services example**:
|
||||
```bash
|
||||
# If using client-side maps
|
||||
NEXT_PUBLIC_GOOGLE_MAPS_KEY="..."
|
||||
|
||||
# If using server-side geocoding
|
||||
GOOGLE_MAPS_SERVER_KEY="..."
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Database Configuration
|
||||
|
||||
```bash
|
||||
# Standard database URL
|
||||
DATABASE_URL="postgresql://user:password@host:5432/dbname"
|
||||
|
||||
# Optional connection pool settings
|
||||
DATABASE_POOL_SIZE="10"
|
||||
DATABASE_POOL_TIMEOUT="30000"
|
||||
|
||||
# Read replicas (optional)
|
||||
DATABASE_READ_URL="postgresql://user:password@read-host:5432/dbname"
|
||||
```
|
||||
|
||||
### Authentication (NextAuth.js)
|
||||
|
||||
```bash
|
||||
# Required for NextAuth
|
||||
NEXTAUTH_URL="https://example.com"
|
||||
NEXTAUTH_SECRET="..." # 32+ character random string
|
||||
|
||||
# OAuth providers (if using)
|
||||
GOOGLE_CLIENT_ID="..."
|
||||
GOOGLE_CLIENT_SECRET="..."
|
||||
|
||||
GITHUB_CLIENT_ID="..."
|
||||
GITHUB_CLIENT_SECRET="..."
|
||||
```
|
||||
|
||||
### JWT Authentication (Custom)
|
||||
|
||||
```bash
|
||||
# JWT secret
|
||||
JWT_SECRET="..." # 32+ character random string
|
||||
|
||||
# Optional: token expiration
|
||||
JWT_EXPIRES_IN="7d"
|
||||
|
||||
# Optional: refresh token secret
|
||||
JWT_REFRESH_SECRET="..." # Different from JWT_SECRET
|
||||
```
|
||||
|
||||
### External APIs
|
||||
|
||||
```bash
|
||||
# OpenAI
|
||||
OPENAI_API_KEY="sk-..."
|
||||
OPENAI_ORG_ID="org-..." # Optional
|
||||
|
||||
# Stripe
|
||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY="pk_live_..."
|
||||
STRIPE_SECRET_KEY="sk_live_..."
|
||||
STRIPE_WEBHOOK_SECRET="whsec_..."
|
||||
|
||||
# SendGrid
|
||||
SENDGRID_API_KEY="SG...."
|
||||
|
||||
# AWS
|
||||
AWS_ACCESS_KEY_ID="..."
|
||||
AWS_SECRET_ACCESS_KEY="..."
|
||||
AWS_REGION="us-east-1"
|
||||
|
||||
# Cloudinary
|
||||
CLOUDINARY_URL="cloudinary://..."
|
||||
```
|
||||
|
||||
### Email Configuration
|
||||
|
||||
```bash
|
||||
# SMTP
|
||||
SMTP_HOST="smtp.gmail.com"
|
||||
SMTP_PORT="587"
|
||||
SMTP_USER="your-email@gmail.com"
|
||||
SMTP_PASSWORD="app-specific-password"
|
||||
|
||||
# Email service API (alternative)
|
||||
SENDGRID_API_KEY="SG...."
|
||||
MAILGUN_API_KEY="..."
|
||||
```
|
||||
|
||||
### Monitoring and Logging
|
||||
|
||||
```bash
|
||||
# Sentry
|
||||
SENTRY_DSN="https://...@sentry.io/..."
|
||||
SENTRY_AUTH_TOKEN="..." # For source maps upload
|
||||
|
||||
# LogRocket
|
||||
LOGROCKET_APP_ID="..."
|
||||
|
||||
# Custom logging
|
||||
LOG_LEVEL="info" # debug, info, warn, error
|
||||
```
|
||||
|
||||
## Environment-Specific Configuration
|
||||
|
||||
### Development (.env.local)
|
||||
|
||||
```bash
|
||||
# Permissive settings for development
|
||||
NODE_ENV="development"
|
||||
|
||||
# Local database
|
||||
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/myapp_dev"
|
||||
|
||||
# Development secrets (can be weak)
|
||||
JWT_SECRET="dev-secret-change-in-production"
|
||||
NEXTAUTH_SECRET="dev-nextauth-secret"
|
||||
|
||||
# Local URLs
|
||||
NEXTAUTH_URL="http://localhost:3000"
|
||||
NEXT_PUBLIC_API_URL="http://localhost:3000"
|
||||
|
||||
# Test API keys
|
||||
STRIPE_SECRET_KEY="sk_test_..."
|
||||
OPENAI_API_KEY="sk-test-..."
|
||||
|
||||
# Debug flags
|
||||
DEBUG="true"
|
||||
LOG_LEVEL="debug"
|
||||
```
|
||||
|
||||
### Staging (.env.staging)
|
||||
|
||||
```bash
|
||||
# Staging environment
|
||||
NODE_ENV="production" # Use production mode
|
||||
|
||||
# Staging database
|
||||
DATABASE_URL="postgresql://user:password@staging-db:5432/myapp_staging"
|
||||
|
||||
# Production-like secrets (but different from prod)
|
||||
JWT_SECRET="staging-secret-32-chars-minimum"
|
||||
NEXTAUTH_SECRET="staging-nextauth-secret"
|
||||
|
||||
# Staging URLs
|
||||
NEXTAUTH_URL="https://staging.example.com"
|
||||
NEXT_PUBLIC_API_URL="https://staging.example.com"
|
||||
|
||||
# Test API keys (same as dev)
|
||||
STRIPE_SECRET_KEY="sk_test_..."
|
||||
OPENAI_API_KEY="sk-test-..."
|
||||
|
||||
# Staging-specific config
|
||||
SENTRY_ENVIRONMENT="staging"
|
||||
LOG_LEVEL="info"
|
||||
```
|
||||
|
||||
### Production (.env.production)
|
||||
|
||||
```bash
|
||||
# Production environment
|
||||
NODE_ENV="production"
|
||||
|
||||
# Production database
|
||||
DATABASE_URL="postgresql://user:strong-password@prod-db:5432/myapp_prod"
|
||||
|
||||
# Strong, random secrets
|
||||
JWT_SECRET="production-secret-use-crypto-random-32-chars-minimum"
|
||||
NEXTAUTH_SECRET="production-nextauth-secret-also-32-chars-minimum"
|
||||
|
||||
# Production URLs
|
||||
NEXTAUTH_URL="https://example.com"
|
||||
NEXT_PUBLIC_API_URL="https://api.example.com"
|
||||
|
||||
# Production API keys
|
||||
STRIPE_SECRET_KEY="sk_live_..."
|
||||
OPENAI_API_KEY="sk-live-..."
|
||||
|
||||
# Production-specific config
|
||||
SENTRY_ENVIRONMENT="production"
|
||||
LOG_LEVEL="warn"
|
||||
|
||||
# Performance optimization
|
||||
DATABASE_POOL_SIZE="20"
|
||||
```
|
||||
|
||||
## Secret Rotation
|
||||
|
||||
### Why Rotate Secrets?
|
||||
|
||||
- **Security best practice**: Limit impact of potential leaks
|
||||
- **Compliance**: Required by some standards (PCI-DSS, HIPAA)
|
||||
- **Team changes**: Rotate after team member departures
|
||||
- **Suspected breach**: Rotate immediately if compromise suspected
|
||||
|
||||
### What to Rotate
|
||||
|
||||
**Critical secrets** (rotate regularly):
|
||||
- JWT/session secrets (every 90 days)
|
||||
- Database passwords (every 90 days)
|
||||
- API keys for financial services (every 90 days)
|
||||
|
||||
**Less critical** (rotate on schedule or as needed):
|
||||
- Third-party API keys (annually or on breach)
|
||||
- OAuth secrets (annually or on breach)
|
||||
|
||||
**Never rotate**:
|
||||
- Public API keys (if they're truly public)
|
||||
- Client IDs (non-secret identifiers)
|
||||
|
||||
### Rotation Process
|
||||
|
||||
**For JWT/session secrets**:
|
||||
|
||||
1. Generate new secret
|
||||
2. Deploy with NEW_JWT_SECRET environment variable
|
||||
3. Update code to validate with both old and new secrets
|
||||
4. After grace period (e.g., 7 days), remove old secret
|
||||
5. Rename NEW_JWT_SECRET to JWT_SECRET
|
||||
|
||||
**For database passwords**:
|
||||
|
||||
1. Create new database user/password
|
||||
2. Deploy with new DATABASE_URL (no downtime)
|
||||
3. Monitor for 24 hours
|
||||
4. Delete old database user
|
||||
5. Update password manager/secrets storage
|
||||
|
||||
**For API keys** (if service supports multiple keys):
|
||||
|
||||
1. Generate new API key (keep old active)
|
||||
2. Deploy with new key
|
||||
3. Monitor for 24 hours
|
||||
4. Revoke old API key
|
||||
|
||||
## Testing Strategies
|
||||
|
||||
### Local Testing
|
||||
|
||||
**Use `.env.test` for test environment**:
|
||||
```bash
|
||||
# .env.test
|
||||
NODE_ENV="test"
|
||||
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/myapp_test"
|
||||
JWT_SECRET="test-secret"
|
||||
```
|
||||
|
||||
**Load in tests**:
|
||||
```typescript
|
||||
// jest.config.js
|
||||
module.exports = {
|
||||
setupFiles: ['<rootDir>/jest.setup.js'],
|
||||
};
|
||||
|
||||
// jest.setup.js
|
||||
import { loadEnvConfig } from '@next/env';
|
||||
loadEnvConfig(process.cwd());
|
||||
```
|
||||
|
||||
### Mock Environment Variables
|
||||
|
||||
```typescript
|
||||
// test/utils/env.ts
|
||||
export function mockEnv(vars: Record<string, string>) {
|
||||
const original = { ...process.env };
|
||||
|
||||
beforeAll(() => {
|
||||
Object.assign(process.env, vars);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env = original;
|
||||
});
|
||||
}
|
||||
|
||||
// In test file
|
||||
import { mockEnv } from './utils/env';
|
||||
|
||||
describe('API', () => {
|
||||
mockEnv({
|
||||
JWT_SECRET: 'test-secret',
|
||||
DATABASE_URL: 'postgresql://localhost/test',
|
||||
});
|
||||
|
||||
// Your tests...
|
||||
});
|
||||
```
|
||||
|
||||
### Validate Before Tests
|
||||
|
||||
```typescript
|
||||
// test/setup.ts
|
||||
import { envSchema } from '@/lib/env';
|
||||
|
||||
try {
|
||||
envSchema.parse(process.env);
|
||||
} catch (error) {
|
||||
console.error('Invalid test environment configuration:');
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
```
|
||||
|
||||
## Deployment Checklist
|
||||
|
||||
### Before Deploying to Production
|
||||
|
||||
- [ ] All required variables are set in production environment
|
||||
- [ ] Secrets are strong (32+ characters, random)
|
||||
- [ ] No secrets in `NEXT_PUBLIC_*` variables
|
||||
- [ ] Database URL points to production database
|
||||
- [ ] API keys are production keys (not test keys)
|
||||
- [ ] `NODE_ENV` is set to `production`
|
||||
- [ ] URLs are production URLs (no localhost)
|
||||
- [ ] `.env.production` is NOT committed to git
|
||||
- [ ] Secrets are stored in secure secret management system
|
||||
- [ ] CI/CD has access to environment variables
|
||||
- [ ] Monitoring/logging is configured (Sentry, etc.)
|
||||
|
||||
### Validation Script
|
||||
|
||||
Run before deployment:
|
||||
```bash
|
||||
python scripts/validate_env.py --file .env.production
|
||||
```
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
**GitHub Actions example**:
|
||||
```yaml
|
||||
- name: Validate environment
|
||||
run: python scripts/validate_env.py --file .env.production
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
JWT_SECRET: ${{ secrets.JWT_SECRET }}
|
||||
# ... other secrets from GitHub Secrets
|
||||
```
|
||||
|
||||
### Post-Deployment Verification
|
||||
|
||||
- [ ] Application starts without errors
|
||||
- [ ] Database connection works
|
||||
- [ ] Authentication works
|
||||
- [ ] External API calls work
|
||||
- [ ] No environment-related errors in logs
|
||||
- [ ] Monitoring shows healthy status
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
### 1. Committing Secrets to Git
|
||||
|
||||
```bash
|
||||
# [X] NEVER DO THIS
|
||||
git add .env.production
|
||||
git commit -m "Add production config" # [ERROR] Secrets in git history
|
||||
```
|
||||
|
||||
**If you accidentally commit secrets**:
|
||||
1. Rotate all exposed secrets immediately
|
||||
2. Use `git filter-branch` or BFG Repo-Cleaner to remove from history
|
||||
3. Force push (if safe to do so)
|
||||
4. Notify team and audit access
|
||||
|
||||
### 2. Exposing Secrets in Public Variables
|
||||
|
||||
```bash
|
||||
# [X] WRONG
|
||||
NEXT_PUBLIC_DATABASE_URL="postgresql://..." # [ERROR] Exposed to client
|
||||
NEXT_PUBLIC_JWT_SECRET="..." # [ERROR] Exposed to client
|
||||
```
|
||||
|
||||
### 3. Weak Secrets
|
||||
|
||||
```bash
|
||||
# [X] WRONG
|
||||
JWT_SECRET="secret" # [ERROR] Too weak
|
||||
JWT_SECRET="password123" # [ERROR] Predictable
|
||||
JWT_SECRET="myapp-secret" # [ERROR] Too short
|
||||
```
|
||||
|
||||
### 4. Copy-Pasting Between Environments
|
||||
|
||||
```bash
|
||||
# [X] WRONG - Same secrets in dev and prod
|
||||
# .env.local
|
||||
JWT_SECRET="abc123..."
|
||||
|
||||
# .env.production
|
||||
JWT_SECRET="abc123..." # [ERROR] Should be different
|
||||
```
|
||||
|
||||
### 5. Hardcoding in Code
|
||||
|
||||
```typescript
|
||||
// [X] WRONG
|
||||
const apiKey = 'sk_live_hardcoded_key'; // [ERROR] Never hardcode
|
||||
|
||||
// [OK] CORRECT
|
||||
const apiKey = process.env.STRIPE_SECRET_KEY;
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Next.js Environment Variables Documentation](https://nextjs.org/docs/basic-features/environment-variables)
|
||||
- [OWASP Secret Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html)
|
||||
- [12-Factor App Config](https://12factor.net/config)
|
||||
- [Vercel Environment Variables](https://vercel.com/docs/concepts/projects/environment-variables)
|
||||
Reference in New Issue
Block a user