Manage multiple development environment profiles (dev/test/staging/prod). Use when developers need to switch between environments, validate configuration, manage secrets, or ensure environment parity.
Streamline management of multiple environment profiles with automated switching, validation, and secret handling.
# Directory structure
.env.development # Development environment
.env.test # Test environment
.env.staging # Staging environment
.env.production # Production environment (never committed)
.env.example # Template with all variables
.env # Current active environment (gitignored)
# Generate: scripts/switch-env.sh
#!/bin/bash
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
NC='\033[0m'
ENVIRONMENT=$1
if [ -z "$ENVIRONMENT" ]; then
echo -e "${RED}❌ Usage: ./scripts/switch-env.sh [dev|test|staging|prod]${NC}"
exit 1
fi
# Map short names to full names
case $ENVIRONMENT in
dev|development)
ENV_FILE=".env.development"
;;
test|testing)
ENV_FILE=".env.test"
;;
stage|staging)
ENV_FILE=".env.staging"
;;
prod|production)
ENV_FILE=".env.production"
;;
*)
echo -e "${RED}❌ Unknown environment: $ENVIRONMENT${NC}"
echo "Valid options: dev, test, staging, prod"
exit 1
;;
esac
# Check if environment file exists
if [ ! -f "$ENV_FILE" ]; then
echo -e "${RED}❌ Environment file not found: $ENV_FILE${NC}"
exit 1
fi
# Backup current .env if it exists
if [ -f ".env" ]; then
cp .env .env.backup
echo -e "${YELLOW}📦 Backed up current .env to .env.backup${NC}"
fi
# Copy environment file
cp "$ENV_FILE" .env
echo -e "${GREEN}✅ Switched to $ENVIRONMENT environment${NC}"
# Show key differences
echo -e "\n${BLUE}📋 Active configuration:${NC}"
grep -E "^[A-Z_]+=.*" .env | head -10
# Validate environment
./scripts/validate-env.sh
echo -e "\n${YELLOW}💡 Restart your services for changes to take effect:${NC}"
echo -e " ${BLUE}make dev-reset${NC}"
### 3. Environment Validator
```typescript
// Generate: scripts/validate-env.ts
import fs from 'fs';
import path from 'path';
interface ValidationRule {
key: string;
required: boolean;
type?: 'string' | 'number' | 'boolean' | 'url' | 'email';
pattern?: RegExp;
enum?: string[];
}
const VALIDATION_RULES: ValidationRule[] = [
// Service Ports
{ key: 'FRONTEND_PORT', required: true, type: 'number' },
{ key: 'API_PORT', required: true, type: 'number' },
{ key: 'POSTGRES_PORT', required: true, type: 'number' },
{ key: 'REDIS_PORT', required: true, type: 'number' },
// Database
{ key: 'DB_NAME', required: true, type: 'string' },
{ key: 'DB_USER', required: true, type: 'string' },
{ key: 'DB_PASSWORD', required: true, type: 'string' },
{ key: 'DATABASE_URL', required: false, type: 'url' },
// Environment
{ key: 'NODE_ENV', required: true, enum: ['development', 'test', 'staging', 'production'] },
{ key: 'LOG_LEVEL', required: false, enum: ['error', 'warn', 'info', 'debug'] },
// URLs
{ key: 'VITE_API_URL', required: true, type: 'url' },
];
function loadEnv(envPath: string = '.env'): Record<string, string> {
const env: Record<string, string> = {};
if (!fs.existsSync(envPath)) {
throw new Error(`Environment file not found: ${envPath}`);
}
const content = fs.readFileSync(envPath, 'utf-8');
const lines = content.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#')) {
const [key, ...valueParts] = trimmed.split('=');
if (key && valueParts.length > 0) {
env[key.trim()] = valueParts.join('=').trim();
}
}
}
return env;
}
function validateValue(key: string, value: string, rule: ValidationRule): string[] {
const errors: string[] = [];
// Type validation
if (rule.type === 'number' && isNaN(Number(value))) {
errors.push(`${key} must be a number`);
}
if (rule.type === 'boolean' && !['true', 'false'].includes(value.toLowerCase())) {
errors.push(`${key} must be true or false`);
}
if (rule.type === 'url') {
try {
new URL(value);
} catch {
errors.push(`${key} must be a valid URL`);
}
}
if (rule.type === 'email' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
errors.push(`${key} must be a valid email`);
}
// Pattern validation
if (rule.pattern && !rule.pattern.test(value)) {
errors.push(`${key} does not match required pattern`);
}
// Enum validation
if (rule.enum && !rule.enum.includes(value)) {
errors.push(`${key} must be one of: ${rule.enum.join(', ')}`);
}
return errors;
}
function validateEnvironment(envPath: string = '.env'): void {
console.log('🔍 Validating environment configuration...\n');
try {
const env = loadEnv(envPath);
const errors: string[] = [];
const warnings: string[] = [];
// Check required variables
for (const rule of VALIDATION_RULES) {
const value = env[rule.key];
if (rule.required && !value) {
errors.push(`Missing required variable: ${rule.key}`);
} else if (value) {
const validationErrors = validateValue(rule.key, value, rule);
errors.push(...validationErrors);
}
}
// Check for insecure defaults in production
if (env.NODE_ENV === 'production') {
const insecureVars = [
'dev_password',
'dev_secret',
'change_in_production',
'localhost',
];
for (const [key, value] of Object.entries(env)) {
for (const insecure of insecureVars) {
if (value.toLowerCase().includes(insecure)) {
warnings.push(`${key} contains insecure value for production: ${value}`);
}
}
}
}
// Display results
if (errors.length === 0) {
console.log('✅ Environment validation passed!');
if (warnings.length > 0) {
console.log('\n⚠️ Warnings:');
warnings.forEach(w => console.log(` • ${w}`));
}
process.exit(0);
} else {
console.log('❌ Environment validation failed!\n');
console.log('Errors:');
errors.forEach(e => console.log(` • ${e}`));
if (warnings.length > 0) {
console.log('\nWarnings:');
warnings.forEach(w => console.log(` • ${w}`));
}
process.exit(1);
}
} catch (error) {
console.error('❌ Validation error:', error);
process.exit(1);
}
}
// Run if executed directly
if (require.main === module) {
const envPath = process.argv[2] || '.env';
validateEnvironment(envPath);
}
export { validateEnvironment, loadEnv };
// Generate: scripts/encrypt-secrets.ts
import crypto from 'crypto';
import fs from 'fs';
const ALGORITHM = 'aes-256-gcm';
const KEY_LENGTH = 32;
const IV_LENGTH = 16;
const TAG_LENGTH = 16;
class SecretManager {
private key: Buffer;
constructor(masterPassword: string) {
// Derive key from master password
this.key = crypto.scryptSync(masterPassword, 'salt', KEY_LENGTH);
}
encrypt(text: string): string {
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv(ALGORITHM, this.key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const tag = cipher.getAuthTag();
// Return: iv:tag:encrypted
return `${iv.toString('hex')}:${tag.toString('hex')}:${encrypted}`;
}
decrypt(encrypted: string): string {
const parts = encrypted.split(':');
if (parts.length !== 3) {
throw new Error('Invalid encrypted format');
}
const [ivHex, tagHex, encryptedText] = parts;
const iv = Buffer.from(ivHex, 'hex');
const tag = Buffer.from(tagHex, 'hex');
const decipher = crypto.createDecipheriv(ALGORITHM, this.key, iv);
decipher.setAuthTag(tag);
let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
encryptFile(inputPath: string, outputPath: string): void {
const content = fs.readFileSync(inputPath, 'utf-8');
const lines = content.split('\n');
const encrypted: string[] = [];
for (const line of lines) {
if (line.trim() && !line.startsWith('#')) {
const [key, ...valueParts] = line.split('=');
if (key && valueParts.length > 0) {
const value = valueParts.join('=');
const encryptedValue = this.encrypt(value);
encrypted.push(`${key}=${encryptedValue}`);
} else {
encrypted.push(line);
}
} else {
encrypted.push(line);
}
}
fs.writeFileSync(outputPath, encrypted.join('\n'));
}
decryptFile(inputPath: string, outputPath: string): void {
const content = fs.readFileSync(inputPath, 'utf-8');
const lines = content.split('\n');
const decrypted: string[] = [];
for (const line of lines) {
if (line.trim() && !line.startsWith('#')) {
const [key, ...valueParts] = line.split('=');
if (key && valueParts.length > 0) {
const encryptedValue = valueParts.join('=');
try {
const value = this.decrypt(encryptedValue);
decrypted.push(`${key}=${value}`);
} catch {
// Not encrypted, keep as-is
decrypted.push(line);
}
} else {
decrypted.push(line);
}
} else {
decrypted.push(line);
}
}
fs.writeFileSync(outputPath, decrypted.join('\n'));
}
}
// CLI usage
if (require.main === module) {
const [,, command, inputFile, outputFile] = process.argv;
const password = process.env.MASTER_PASSWORD;
if (!password) {
console.error('❌ Set MASTER_PASSWORD environment variable');
process.exit(1);
}
if (!command || !inputFile) {
console.error('Usage: MASTER_PASSWORD=xxx ts-node encrypt-secrets.ts [encrypt|decrypt] <input> <output>');
process.exit(1);
}
const manager = new SecretManager(password);
try {
if (command === 'encrypt') {
manager.encryptFile(inputFile, outputFile || `${inputFile}.encrypted`);
console.log('✅ Secrets encrypted');
} else if (command === 'decrypt') {
manager.decryptFile(inputFile, outputFile || `${inputFile}.decrypted`);
console.log('✅ Secrets decrypted');
} else {
console.error('Unknown command:', command);
process.exit(1);
}
} catch (error) {
console.error('❌ Error:', error);
process.exit(1);
}
}
export { SecretManager };
# Generate: scripts/compare-envs.sh
#!/bin/bash
ENV1=${1:-.env.development}
ENV2=${2:-.env.production}
echo "📊 Comparing $ENV1 vs $ENV2"
echo ""
# Get all unique keys
KEYS=$(cat "$ENV1" "$ENV2" | grep -E "^[A-Z_]+" | cut -d'=' -f1 | sort | uniq)
echo "Key | $ENV1 | $ENV2"
echo "--------------------------------------------------------"
for key in $KEYS; do
val1=$(grep "^$key=" "$ENV1" 2>/dev/null | cut -d'=' -f2-)
val2=$(grep "^$key=" "$ENV2" 2>/dev/null | cut -d'=' -f2-)
if [ "$val1" = "$val2" ]; then
status="✓"
elif [ -z "$val1" ]; then
status="Missing in $ENV1"
elif [ -z "$val2" ]; then
status="Missing in $ENV2"
else
status="Different"
fi
printf "%-28s | %-10s | %s\n" "$key" "${val1:0:10}" "${val2:0:10}"
done
# Generate: .env.development
# Development Environment Configuration
NODE_ENV=development
LOG_LEVEL=debug
# Service Ports
FRONTEND_PORT=3000
API_PORT=8000
POSTGRES_PORT=5432
REDIS_PORT=6379
# Database
DB_NAME=dev_db
DB_USER=dev_user
DB_PASSWORD=dev_password
DATABASE_URL=postgres://dev_user:dev_password@localhost:5432/dev_db
# Redis
REDIS_URL=redis://localhost:6379
# URLs
VITE_API_URL=http://localhost:8000
# Feature Flags
ENABLE_DEBUG=true
ENABLE_HOT_RELOAD=true
ENABLE_SOURCE_MAPS=true
# Development Settings
SEED_DATABASE=true
# Generate: .env.test
# Test Environment Configuration
NODE_ENV=test
LOG_LEVEL=error
# Service Ports
FRONTEND_PORT=3001
API_PORT=8001
POSTGRES_PORT=5433
REDIS_PORT=6380
# Database
DB_NAME=test_db
DB_USER=test_user
DB_PASSWORD=test_password
DATABASE_URL=postgres://test_user:test_password@localhost:5433/test_db
# Redis
REDIS_URL=redis://localhost:6380
# URLs
VITE_API_URL=http://localhost:8001
# Feature Flags
ENABLE_DEBUG=false
ENABLE_HOT_RELOAD=false
ENABLE_SOURCE_MAPS=true
# Test Settings
SEED_DATABASE=true
TEST_TIMEOUT=5000
.PHONY: env-switch env-validate env-compare
env-switch: ## Switch environment profile
@./scripts/switch-env.sh $(ENV)
env-validate: ## Validate current environment
@npm run env:validate
env-compare: ## Compare two environment files
@./scripts/compare-envs.sh $(ENV1) $(ENV2)
# Example usage:
# make env-switch ENV=prod
# make env-compare ENV1=.env.development ENV2=.env.staging
When both skills are active:
Variables not loading:
# Check file format
cat -A .env | head
# Validate syntax
./scripts/validate-env.sh
Permission errors:
chmod 600 .env
chmod 600 .env.*