Squire config-manager
install
source · Clone the upstream repo
git clone https://github.com/eddiebelaval/squire
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/eddiebelaval/squire "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/config-manager" ~/.claude/skills/eddiebelaval-squire-config-manager && rm -rf "$T"
manifest:
skills/config-manager/SKILL.mdsource content
name: config-manager description: Expert guide for managing application configuration including environment variables, config files, secrets management, and multi-environment setups. Use when handling .env files, config validation, or configuration architecture. slug: config-manager category: operations complexity: complex version: "1.0.0" author: "id8Labs" triggers:
- "config-manager"
- "config manager" tags:
- development
- tool-factory-retrofitted---
Configuration Manager Skill
Core Workflows
Workflow 1: Primary Action
- Analyze the input and context
- Validate prerequisites are met
- Execute the core operation
- Verify the output meets expectations
- Report results
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 # =================== # Email # =================== 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<typeof envSchema>; // 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<typeof configSchema>; 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<AppSecrets> { if (cachedSecrets) { return cachedSecrets; } 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<string, FeatureConfig> = { newCheckout: { enabled: ['development', 'staging'], description: 'New checkout flow', }, aiAssistant: { enabled: true, description: 'AI assistant feature', }, experimentalApi: { enabled: ['development'], description: 'Experimental API endpoints', }, }; 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<string, any> = new Map(); get<T>(key: string, defaultValue?: T): T | undefined { return this.values.get(key) ?? defaultValue; } set<T>(key: string, value: T): void { const oldValue = this.values.get(key); this.values.set(key, value); this.emit('change', { key, oldValue, newValue: value }); } // 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
-
with all variables documented.env.example -
excludes all.gitignore
files except example.env - 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