Configuration Manager Skill Overview
This skill helps you design and implement robust configuration management for applications. Covers environment variables, config files, secrets management, validation, type safety, and multi-environment deployments.
Configuration Philosophy Twelve-Factor App Principles Store config in the environment: Separate config from code Strict separation: Config that varies between deploys should be in env vars No secrets in code: Ever. Period. Configuration Hierarchy Priority (highest to lowest): 1. Command-line arguments 2. Environment variables 3. Environment-specific config files (.env.local) 4. Default config files (.env) 5. Application defaults in code
What Goes Where Environment Variables: API keys, database URLs, feature flags Config Files: Non-sensitive defaults, complex structures Secrets Manager: Production credentials, API tokens Code: Application logic, never secrets Environment Variables .env File Structure
.env.example - Commit this file as documentation
Copy to .env and fill in values
===================
Application
===================
NODE_ENV=development PORT=3000 APP_URL=http://localhost:3000
===================
Database
===================
DATABASE_URL=postgresql://user:password@localhost:5432/mydb DATABASE_POOL_SIZE=10
===================
Authentication
===================
Get from Supabase Dashboard > Settings > API
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
===================
External Services
===================
STRIPE_SECRET_KEY=sk_test_xxx STRIPE_WEBHOOK_SECRET=whsec_xxx NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx
===================
===================
RESEND_API_KEY=re_xxx EMAIL_FROM=noreply@example.com
===================
Feature Flags
===================
ENABLE_ANALYTICS=true ENABLE_BETA_FEATURES=false
.gitignore Configuration
Environment files
.env .env.local .env.*.local .env.development.local .env.test.local .env.production.local
Keep example file
!.env.example
Multi-Environment Setup
.env.development
NODE_ENV=development DATABASE_URL=postgresql://localhost:5432/myapp_dev LOG_LEVEL=debug
.env.test
NODE_ENV=test DATABASE_URL=postgresql://localhost:5432/myapp_test LOG_LEVEL=error
.env.production
NODE_ENV=production DATABASE_URL=${DATABASE_URL} # Set in deployment platform LOG_LEVEL=info
Type-Safe Configuration Zod Schema Validation // src/config/env.ts import { z } from 'zod';
// Define schema const envSchema = z.object({ // Node environment NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
// Server PORT: z.coerce.number().default(3000), APP_URL: z.string().url(),
// Database DATABASE_URL: z.string().url(), DATABASE_POOL_SIZE: z.coerce.number().min(1).max(100).default(10),
// Supabase NEXT_PUBLIC_SUPABASE_URL: z.string().url(), NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1), SUPABASE_SERVICE_ROLE_KEY: z.string().min(1),
// Stripe (optional in development) STRIPE_SECRET_KEY: z.string().startsWith('sk_').optional(), STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_').optional(),
// Feature flags ENABLE_ANALYTICS: z.coerce.boolean().default(false), ENABLE_BETA_FEATURES: z.coerce.boolean().default(false), });
// Type export
export type Env = z.infer
// Parse and validate function loadEnv(): Env { const result = envSchema.safeParse(process.env);
if (!result.success) { console.error('Invalid environment variables:'); console.error(result.error.format()); process.exit(1); }
return result.data; }
// Singleton export export const env = loadEnv();
Environment Validation at Build Time // src/config/validate-env.ts import { z } from 'zod';
// Client-side env (exposed to browser) const clientEnvSchema = z.object({ NEXT_PUBLIC_SUPABASE_URL: z.string().url(), NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1), NEXT_PUBLIC_APP_URL: z.string().url(), });
// Server-side env (never exposed to browser) const serverEnvSchema = z.object({ DATABASE_URL: z.string(), SUPABASE_SERVICE_ROLE_KEY: z.string(), STRIPE_SECRET_KEY: z.string().optional(), });
// Combined const envSchema = clientEnvSchema.merge(serverEnvSchema);
export function validateEnv() { // Only validate server-side env on server if (typeof window !== 'undefined') { return clientEnvSchema.parse({ NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL, }); }
return envSchema.parse(process.env); }
T3 Env Pattern (Next.js) // src/env.mjs import { createEnv } from '@t3-oss/env-nextjs'; import { z } from 'zod';
export const env = createEnv({ /* * Server-side environment variables / server: { DATABASE_URL: z.string().url(), NODE_ENV: z.enum(['development', 'test', 'production']), SUPABASE_SERVICE_ROLE_KEY: z.string(), STRIPE_SECRET_KEY: z.string().startsWith('sk_'), },
/* * Client-side environment variables (exposed to browser) * Prefix with NEXT_PUBLIC_ / client: { NEXT_PUBLIC_SUPABASE_URL: z.string().url(), NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string(), NEXT_PUBLIC_APP_URL: z.string().url(), },
/* * Runtime values / runtimeEnv: { DATABASE_URL: process.env.DATABASE_URL, NODE_ENV: process.env.NODE_ENV, SUPABASE_SERVICE_ROLE_KEY: process.env.SUPABASE_SERVICE_ROLE_KEY, STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY, NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL, },
/* * Skip validation in certain environments / skipValidation: !!process.env.SKIP_ENV_VALIDATION, });
Configuration Files JSON Configuration // config/default.json { "app": { "name": "My Application", "version": "1.0.0" }, "server": { "port": 3000, "host": "localhost" }, "cache": { "ttl": 3600, "maxSize": 1000 }, "features": { "darkMode": true, "betaFeatures": false } }
// config/production.json (overrides default) { "server": { "host": "0.0.0.0" }, "cache": { "ttl": 86400 } }
Config Loader // src/config/loader.ts import { readFileSync, existsSync } from 'fs'; import { join } from 'path'; import { z } from 'zod';
const configSchema = z.object({ app: z.object({ name: z.string(), version: z.string(), }), server: z.object({ port: z.number(), host: z.string(), }), cache: z.object({ ttl: z.number(), maxSize: z.number(), }), features: z.object({ darkMode: z.boolean(), betaFeatures: z.boolean(), }), });
type Config = z.infer
function deepMerge(target: any, source: any): any { const result = { ...target };
for (const key of Object.keys(source)) { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { result[key] = deepMerge(result[key] || {}, source[key]); } else { result[key] = source[key]; } }
return result; }
export function loadConfig(): Config { const env = process.env.NODE_ENV || 'development'; const configDir = join(process.cwd(), 'config');
// Load default config const defaultPath = join(configDir, 'default.json'); let config = JSON.parse(readFileSync(defaultPath, 'utf-8'));
// Merge environment-specific config
const envPath = join(configDir, ${env}.json);
if (existsSync(envPath)) {
const envConfig = JSON.parse(readFileSync(envPath, 'utf-8'));
config = deepMerge(config, envConfig);
}
// Merge local overrides (not committed) const localPath = join(configDir, 'local.json'); if (existsSync(localPath)) { const localConfig = JSON.parse(readFileSync(localPath, 'utf-8')); config = deepMerge(config, localConfig); }
// Validate return configSchema.parse(config); }
export const config = loadConfig();
Secrets Management Local Development Secrets
Use a secrets manager locally too
Option 1: 1Password CLI
op read "op://Development/MyApp/API_KEY"
Option 2: Doppler
doppler secrets download --no-file --format env > .env
Option 3: AWS SSM (for local AWS dev)
aws ssm get-parameter --name "/myapp/dev/api-key" --with-decryption --query "Parameter.Value"
Production Secrets with Vercel
Add secrets via CLI
vercel env add STRIPE_SECRET_KEY production vercel env add DATABASE_URL production
Pull secrets to local .env
vercel env pull .env.local
Secrets in Docker
docker-compose.yml
version: '3.8' services: app: build: . environment: - NODE_ENV=production env_file: - .env secrets: - db_password - api_key
secrets: db_password: file: ./secrets/db_password.txt api_key: external: true # From Docker Swarm/secrets manager
AWS Secrets Manager Integration // src/config/secrets.ts import { SecretsManagerClient, GetSecretValueCommand, } from '@aws-sdk/client-secrets-manager';
const client = new SecretsManagerClient({ region: 'us-east-1' });
interface AppSecrets { database_url: string; stripe_secret_key: string; jwt_secret: string; }
let cachedSecrets: AppSecrets | null = null;
export async function getSecrets(): Promise
const secretId = process.env.AWS_SECRET_ID || 'myapp/production/secrets';
const command = new GetSecretValueCommand({ SecretId: secretId }); const response = await client.send(command);
if (!response.SecretString) { throw new Error('Secret not found'); }
cachedSecrets = JSON.parse(response.SecretString); return cachedSecrets!; }
// Usage async function connectDatabase() { const secrets = await getSecrets(); return createConnection(secrets.database_url); }
Feature Flags Simple Feature Flag System // src/config/features.ts import { env } from './env';
export const features = { analytics: env.ENABLE_ANALYTICS, betaFeatures: env.ENABLE_BETA_FEATURES, darkMode: true, // Always enabled
// Computed flags get isProduction() { return env.NODE_ENV === 'production'; },
get enableDebugLogs() { return env.NODE_ENV === 'development' || env.DEBUG === 'true'; }, } as const;
// Type-safe feature checking export function isEnabled(feature: keyof typeof features): boolean { return Boolean(features[feature]); }
// Usage if (isEnabled('analytics')) { initAnalytics(); }
Environment-Aware Feature Flags // src/config/feature-flags.ts type Environment = 'development' | 'staging' | 'production';
interface FeatureConfig { enabled: boolean | Environment[]; description: string; }
const featureFlags: Record
export function isFeatureEnabled( feature: keyof typeof featureFlags ): boolean { const config = featureFlags[feature]; const currentEnv = process.env.NODE_ENV as Environment;
if (typeof config.enabled === 'boolean') { return config.enabled; }
return config.enabled.includes(currentEnv); }
Configuration Patterns Singleton Configuration // src/config/index.ts import { env } from './env'; import { loadConfig } from './loader'; import { features } from './features';
class AppConfig { private static instance: AppConfig;
public readonly env = env; public readonly features = features; public readonly settings = loadConfig();
private constructor() { // Freeze to prevent modifications Object.freeze(this); }
static getInstance(): AppConfig { if (!AppConfig.instance) { AppConfig.instance = new AppConfig(); } return AppConfig.instance; }
// Helper methods get isDevelopment(): boolean { return this.env.NODE_ENV === 'development'; }
get isProduction(): boolean { return this.env.NODE_ENV === 'production'; }
get databaseUrl(): string { return this.env.DATABASE_URL; } }
export const config = AppConfig.getInstance();
Runtime Configuration Updates // src/config/runtime.ts import { EventEmitter } from 'events';
class RuntimeConfig extends EventEmitter {
private values: Map
get
set
// Subscribe to changes onChange(callback: (change: { key: string; oldValue: any; newValue: any }) => void) { this.on('change', callback); return () => this.off('change', callback); } }
export const runtimeConfig = new RuntimeConfig();
// Usage: Update config at runtime runtimeConfig.set('maintenanceMode', true);
// Usage: React to changes runtimeConfig.onChange(({ key, newValue }) => { if (key === 'maintenanceMode' && newValue) { showMaintenanceBanner(); } });
Validation Checklist Environment Setup .env.example with all variables documented .gitignore excludes all .env files except example Validation runs at application startup Helpful error messages for missing/invalid config Type definitions for all config values Security No secrets in version control Secrets use appropriate secrets manager Production secrets rotated regularly Minimal exposure of sensitive values in logs NEXT_PUBLIC_ prefix only for truly public values Developer Experience Easy local setup (copy .env.example) Clear documentation of each variable Defaults for development environment Config validation gives actionable errors IDE autocomplete for config values Multi-Environment Separate configs for dev/staging/production Environment-specific overrides work correctly Feature flags for gradual rollouts Easy switching between environments When to Use This Skill
Invoke this skill when:
Setting up configuration for a new project Adding new environment variables Implementing secrets management Creating feature flag systems Debugging configuration issues Setting up multi-environment deployments Migrating configuration between providers Implementing runtime configuration changes