feat: implement unified configuration as single source of truth

- Make unified config lazy-loaded to avoid initialization issues
- Replace direct config exports with getter functions
- Rewrite utils/env.ts to use unified config instead of scattered access
- Add show-config.js helper script for configuration management
- Type-safe configuration access throughout the app
- Smart defaults for all environments with environment overrides
- Eliminates scattered process.env and import.meta.env access
This commit is contained in:
William Valentin
2025-09-08 21:23:44 -07:00
parent 8830842ba2
commit b59160eb10
3 changed files with 330 additions and 89 deletions

View File

@@ -809,24 +809,33 @@ export function createUnifiedConfigForEnvironment(
}
/**
* Singleton configuration instance
* Lazy-loaded singleton configuration instance
*/
export const unifiedConfig = createUnifiedConfig();
let _unifiedConfig: UnifiedConfig | null = null;
export const unifiedConfig: UnifiedConfig = new Proxy({} as UnifiedConfig, {
get(target, prop) {
if (!_unifiedConfig) {
_unifiedConfig = createUnifiedConfig();
}
return _unifiedConfig[prop as keyof UnifiedConfig];
},
});
/**
* Export specific configuration sections for convenience
*/
export const appConfig = unifiedConfig.app;
export const databaseConfig = unifiedConfig.database;
export const containerConfig = unifiedConfig.container;
export const kubernetesConfig = unifiedConfig.kubernetes;
export const authConfig = unifiedConfig.auth;
export const emailConfig = unifiedConfig.email;
export const oauthConfig = unifiedConfig.oauth;
export const featureFlags = unifiedConfig.features;
export const performanceConfig = unifiedConfig.performance;
export const loggingConfig = unifiedConfig.logging;
export const securityConfig = unifiedConfig.security;
export const getAppConfig = () => unifiedConfig.app;
export const getDatabaseConfig = () => unifiedConfig.database;
export const getContainerConfig = () => unifiedConfig.container;
export const getKubernetesConfig = () => unifiedConfig.kubernetes;
export const getAuthConfig = () => unifiedConfig.auth;
export const getEmailConfig = () => unifiedConfig.email;
export const getOAuthConfig = () => unifiedConfig.oauth;
export const getFeatureFlags = () => unifiedConfig.features;
export const getPerformanceConfig = () => unifiedConfig.performance;
export const getLoggingConfig = () => unifiedConfig.logging;
export const getSecurityConfig = () => unifiedConfig.security;
/**
* Utility functions

187
show-config.js Normal file
View File

@@ -0,0 +1,187 @@
#!/usr/bin/env bun
/**
* Configuration Helper Script
*
* This script shows the current unified configuration and provides
* examples of environment variables that can be set to override defaults.
*
* Usage:
* bun show-config.js # Show current config
* bun show-config.js --env # Show as environment variables
* bun show-config.js --help # Show help
*/
/* eslint-disable no-console */
import { unifiedConfig, exportAsEnvVars } from './config/unified.config';
const args = process.argv.slice(2);
const showEnv = args.includes('--env');
const showHelp = args.includes('--help');
if (showHelp) {
console.log(`
Configuration Helper - Single Source of Truth
USAGE:
bun show-config.js Show current unified config
bun show-config.js --env Show as environment variables
bun show-config.js --help Show this help
DESCRIPTION:
This app uses a unified configuration system as the single source of truth.
All settings come from config/unified.config.ts with environment overrides.
COMMON ENVIRONMENT OVERRIDES:
# Application
NODE_ENV=production
APP_NAME="My Custom App Name"
APP_BASE_URL=https://myapp.com
# Database (required for production)
VITE_COUCHDB_URL=http://localhost:5984
VITE_COUCHDB_USER=admin
VITE_COUCHDB_PASSWORD=secure-password
# Authentication (required for production)
JWT_SECRET=your-secure-jwt-secret
SESSION_SECRET=your-secure-session-secret
# Email (optional)
VITE_MAILGUN_API_KEY=key-abc123
VITE_MAILGUN_DOMAIN=mg.example.com
# OAuth (optional)
VITE_GOOGLE_CLIENT_ID=your-google-client-id
VITE_GITHUB_CLIENT_ID=your-github-client-id
HOW IT WORKS:
1. Unified config provides smart defaults for all environments
2. Environment variables override specific settings when needed
3. No more scattered .env files or hardcoded values
4. Type-safe configuration throughout the app
EXAMPLES:
# Development with custom database
VITE_COUCHDB_URL=http://dev-db:5984 bun run dev
# Production build with custom settings
NODE_ENV=production APP_NAME="Prod App" make docker-build
# Check current config
bun show-config.js
`);
process.exit(0);
}
console.log('🔧 Unified Configuration - Single Source of Truth\n');
if (showEnv) {
console.log('📋 Current Configuration as Environment Variables:\n');
const envVars = exportAsEnvVars();
// Group by category for better readability
const categories = {
Application: [
'APP_NAME',
'APP_VERSION',
'APP_BASE_URL',
'NODE_ENV',
'PORT',
],
Database: [
'VITE_COUCHDB_URL',
'VITE_COUCHDB_USER',
'VITE_COUCHDB_PASSWORD',
'COUCHDB_DATABASE_NAME',
],
Authentication: ['JWT_SECRET', 'JWT_EXPIRES_IN', 'SESSION_SECRET'],
Email: [
'EMAIL_PROVIDER',
'VITE_MAILGUN_API_KEY',
'VITE_MAILGUN_DOMAIN',
'MAILGUN_FROM_NAME',
'MAILGUN_FROM_EMAIL',
],
OAuth: [
'VITE_GOOGLE_CLIENT_ID',
'GOOGLE_CLIENT_SECRET',
'VITE_GITHUB_CLIENT_ID',
'GITHUB_CLIENT_SECRET',
],
Features: ['ENABLE_EMAIL_VERIFICATION', 'ENABLE_OAUTH', 'DEBUG_MODE'],
Logging: ['LOG_LEVEL', 'LOG_FORMAT'],
};
Object.entries(categories).forEach(([category, keys]) => {
console.log(`${category}:`);
keys.forEach(key => {
if (envVars[key] !== undefined) {
const value = envVars[key];
// Mask sensitive values
const maskedValue =
key.includes('SECRET') ||
key.includes('PASSWORD') ||
key.includes('API_KEY')
? '***masked***'
: value;
console.log(` ${key}=${maskedValue}`);
}
});
console.log('');
});
} else {
console.log('📊 Current Unified Configuration:\n');
// Show key configuration sections
console.log('Application:');
console.log(` Name: ${unifiedConfig.app.name}`);
console.log(` Environment: ${unifiedConfig.app.environment}`);
console.log(` Base URL: ${unifiedConfig.app.baseUrl}`);
console.log(` Port: ${unifiedConfig.app.port}`);
console.log('');
console.log('Database:');
console.log(` URL: ${unifiedConfig.database.url}`);
console.log(` Username: ${unifiedConfig.database.username}`);
console.log(
` Password: ${unifiedConfig.database.password ? '***set***' : '***not set***'}`
);
console.log(` Mock Mode: ${unifiedConfig.database.useMock}`);
console.log('');
console.log('Authentication:');
console.log(
` JWT Secret: ${unifiedConfig.auth.jwtSecret ? '***set***' : '***not set***'}`
);
console.log(` JWT Expires: ${unifiedConfig.auth.jwtExpiresIn}`);
console.log('');
console.log('Features:');
console.log(
` Email Verification: ${unifiedConfig.features.enableEmailVerification}`
);
console.log(` OAuth: ${unifiedConfig.features.enableOAuth}`);
console.log(` Debug Mode: ${unifiedConfig.features.debugMode}`);
console.log('');
console.log('Email:');
console.log(` Provider: ${unifiedConfig.email.provider}`);
console.log(
` From: ${unifiedConfig.email.fromName} <${unifiedConfig.email.fromEmail}>`
);
console.log('');
console.log('Logging:');
console.log(` Level: ${unifiedConfig.logging.level}`);
console.log(` Format: ${unifiedConfig.logging.format}`);
console.log('');
}
console.log('💡 Tips:');
console.log(' • Use --env to see current config as environment variables');
console.log(' • Use --help to see configuration documentation');
console.log(' • Set environment variables to override defaults');
console.log(' • All configuration comes from config/unified.config.ts');
console.log('');

View File

@@ -1,59 +1,29 @@
// Environment utility to safely access environment variables
// Compatible with both Vite (browser) and Node.js (Jest/server)
/**
* Environment Utilities - Single Source of Truth
*
* This module provides environment utilities that use the unified config
* as the single source of truth for all environment variables.
*
* Use this instead of directly accessing process.env or import.meta.env
*/
export interface EnvConfig {
VITE_COUCHDB_URL?: string;
VITE_COUCHDB_USERNAME?: string;
VITE_COUCHDB_PASSWORD?: string;
VITE_MAILGUN_API_KEY?: string;
VITE_MAILGUN_DOMAIN?: string;
VITE_MAILGUN_BASE_URL?: string;
VITE_MAILGUN_FROM_NAME?: string;
VITE_MAILGUN_FROM_EMAIL?: string;
NODE_ENV?: string;
COUCHDB_URL?: string;
[key: string]: string | undefined;
}
import {
getAppConfig,
getDatabaseConfig,
getAuthConfig,
getEmailConfig,
getOAuthConfig,
getFeatureFlags,
getLoggingConfig,
getSecurityConfig,
unifiedConfig,
} from '../config/unified.config';
/**
* Safely get environment variables from various sources
* Works in both browser (Vite) and Node.js (Jest/server) environments
* Get the current environment
*/
export function getEnv(): EnvConfig {
let env: EnvConfig = {};
// Try to get from import.meta.env (Vite/browser)
try {
if (typeof globalThis !== 'undefined' && 'import' in globalThis) {
const importMeta = (
globalThis as { import?: { meta?: { env?: Record<string, string> } } }
).import?.meta;
if (importMeta?.env) {
env = { ...env, ...importMeta.env };
}
}
} catch (_e) {
// Ignore errors accessing import.meta
}
// Try to get from process.env (Node.js)
try {
if (typeof process !== 'undefined' && process.env) {
env = { ...env, ...process.env };
}
} catch (_e) {
// Ignore errors accessing process.env
}
return env;
}
/**
* Get a specific environment variable with optional fallback
*/
export function getEnvVar(key: string, fallback?: string): string | undefined {
const env = getEnv();
return env[key] || fallback;
export function getEnvironment(): string {
return getAppConfig().environment;
}
/**
@@ -74,35 +44,110 @@ export function isNode(): boolean {
* Check if we're running in a test environment
*/
export function isTest(): boolean {
const env = getEnv();
return getAppConfig().environment === 'test';
}
// Check for Jest environment
if (
typeof global !== 'undefined' &&
'expect' in global &&
'describe' in global
) {
return true;
}
// Check for Node.js test environment variables
if (typeof process !== 'undefined' && process.env) {
if (process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID) {
return true;
}
}
// Check environment variables
return env.NODE_ENV === 'test';
/**
* Check if we're running in development
*/
export function isDevelopment(): boolean {
return getAppConfig().environment === 'development';
}
/**
* Check if we're running in production
*/
export function isProduction(): boolean {
const env = getEnv();
return (
env.NODE_ENV === 'production' ||
(typeof process !== 'undefined' && process.env?.NODE_ENV === 'production')
);
return getAppConfig().environment === 'production';
}
/**
* Check if we're running in staging
*/
export function isStaging(): boolean {
return getAppConfig().environment === 'staging';
}
/**
* Get application configuration
*/
export { getAppConfig };
/**
* Get database configuration
*/
export { getDatabaseConfig };
/**
* Get authentication configuration
*/
export { getAuthConfig };
/**
* Get email configuration
*/
export { getEmailConfig };
/**
* Get OAuth configuration
*/
export { getOAuthConfig };
/**
* Get feature flags
*/
export { getFeatureFlags };
/**
* Get logging configuration
*/
export { getLoggingConfig };
/**
* Get security configuration
*/
export { getSecurityConfig };
/**
* Get the full unified configuration
* Use this sparingly - prefer the specific getters above
*/
export function getUnifiedConfig() {
return unifiedConfig;
}
/**
* Legacy compatibility - get a specific environment variable
* @deprecated Use the unified config getters instead
*/
export function getEnvVar(key: string, fallback?: string): string | undefined {
console.warn(
`getEnvVar('${key}') is deprecated. Use unified config instead.`
);
// Try to map common environment variables to unified config
switch (key) {
case 'NODE_ENV':
return getAppConfig().environment;
case 'APP_NAME':
case 'VITE_APP_NAME':
return getAppConfig().name;
case 'APP_BASE_URL':
return getAppConfig().baseUrl;
case 'VITE_COUCHDB_URL':
case 'COUCHDB_URL':
return getDatabaseConfig().url;
case 'VITE_COUCHDB_USER':
case 'COUCHDB_USER':
return getDatabaseConfig().username;
case 'VITE_COUCHDB_PASSWORD':
case 'COUCHDB_PASSWORD':
return getDatabaseConfig().password;
case 'JWT_SECRET':
return getAuthConfig().jwtSecret;
case 'LOG_LEVEL':
return getLoggingConfig().level;
default:
return fallback;
}
}