@happyvertical/smrt-config
Centralized configuration management for SMRT modules and applications with support for multiple file formats, environment variables, and powerful orchestration via top-level await.
Overview
The smrt-config package provides a flexible, type-safe configuration system for SMRT applications and modules. It supports multiple configuration file formats, environment variables, runtime overrides, and seamless integration across monorepos.
Key Features
- Multi-format support - JS, TS, JSON, YAML, TOML with auto-detection
- Type-safe TypeScript - Full type safety with
defineConfig() - Secure secrets handling - Auto-detects and sanitizes sensitive data
- Remote configuration - Load from APIs with top-level await
- Configuration merging - Priority hierarchy for flexible overrides
- Monorepo support - globalThis-based caching for package sharing
- Three-tier scoping - Global, package-level, and module-level configs
Installation
npm install @happyvertical/smrt-config
# or
pnpm add @happyvertical/smrt-config
# or
bun add @happyvertical/smrt-configQuick Start (5 Minutes)
1. Create Configuration File
Create smrt.config.js in your project root:
export default {
smrt: {
logLevel: 'info',
cacheDir: '.cache',
environment: 'development'
},
packages: {
ai: {
defaultProvider: 'anthropic',
defaultModel: 'claude-3-5-sonnet-20241022'
}
},
modules: {
'my-scraper': {
cronSchedule: '0 0 * * *',
maxPages: 100
}
}
};2. Load Configuration
In your application entry point:
import { loadConfig } from '@happyvertical/smrt-config';
// Load config at startup
await loadConfig();
console.log('Configuration loaded successfully');3. Use Configuration
Access config in your modules or packages:
import { getPackageConfig, getModuleConfig } from '@happyvertical/smrt-config';
// Get package-level config with defaults
const aiConfig = getPackageConfig('ai', {
defaultProvider: 'openai',
defaultModel: 'gpt-4'
});
// Get module-level config with defaults
const scraperConfig = getModuleConfig('my-scraper', {
cronSchedule: '0 6 * * *',
maxPages: 50
});
console.log(`Using AI model: ${aiConfig.defaultModel}`);
console.log(`Scraper runs at: ${scraperConfig.cronSchedule}`);Architecture
Configuration Scopes
smrt-config supports three hierarchical scopes:
- Global scope (
smrt.*) - Framework-wide settings like log level, cache directory - Package scope (
packages.*) - Per-package settings for reusable packages - Module scope (
modules.*) - Application-specific module configurations
Merging Priority
Configuration sources are merged with this priority (highest to lowest):
- Runtime config - Set via
setConfig() - Environment variables -
SMRT_*prefix - File-based config -
smrt.config.*files - Module/package defaults - Provided in code
File Discovery
Searches upward from current directory for smrt.config.* files. Supports
multiple formats:
.js,.mjs- JavaScript with ESM.ts- TypeScript (auto-compiled).json- JSON.yaml,.yml- YAML.toml- TOML
API Reference
Core Functions
loadConfig(options?): Promise<SmrtConfig>
Load configuration from file and cache it.
import { loadConfig } from '@happyvertical/smrt-config';
// Basic usage
await loadConfig();
// With options
await loadConfig({
configPath: './custom.config.js', // Custom path
searchParents: true, // Search up directory tree
cache: true // Cache the result
});getConfig(): SmrtConfig | null
Get the currently loaded configuration from cache.
import { getConfig } from '@happyvertical/smrt-config';
const config = getConfig();
if (config) {
console.log('Log level:', config.smrt?.logLevel);
}getModuleConfig<T>(moduleName, defaults?): T
Get module-specific configuration with defaults.
import { getModuleConfig } from '@happyvertical/smrt-config';
interface MyModuleConfig {
enabled: boolean;
cronSchedule: string;
maxRetries: number;
}
const config = getModuleConfig<MyModuleConfig>('my-module', {
enabled: true,
cronSchedule: '0 0 * * *',
maxRetries: 3
});
// Merges: defaults < global config < module config < runtime configgetPackageConfig<T>(packageName, defaults?): T
Get package-specific configuration with defaults.
import { getPackageConfig } from '@happyvertical/smrt-config';
interface AIConfig {
defaultProvider: string;
defaultModel: string;
temperature: number;
}
const aiConfig = getPackageConfig<AIConfig>('ai', {
defaultProvider: 'openai',
defaultModel: 'gpt-4',
temperature: 0.7
});setConfig(config): void
Set runtime configuration (highest priority).
import { setConfig } from '@happyvertical/smrt-config';
// Override for testing or dynamic configuration
setConfig({
smrt: { logLevel: 'debug' },
packages: {
ai: { defaultModel: 'gpt-4-turbo' }
}
});clearCache(): void
Clear all cached configuration.
import { clearCache } from '@happyvertical/smrt-config';
// Useful for testing or hot-reload
clearCache();defineConfig(config): SmrtConfig
Helper for type-safe configuration in TypeScript.
import { defineConfig } from '@happyvertical/smrt-config';
// In smrt.config.ts
export default defineConfig({
smrt: {
logLevel: 'info', // TypeScript autocomplete works here
cacheDir: '.cache'
}
});Export Utilities
sanitizeConfig(config): unknown
Remove secrets from configuration.
import { sanitizeConfig } from '@happyvertical/smrt-config';
const config = {
apiKey: 'secret-key',
password: 'secret-password',
normalValue: 'safe'
};
const clean = sanitizeConfig(config);
// Result: { apiKey: '[REDACTED]', password: '[REDACTED]', normalValue: 'safe' }exportConfig(config, options?): string
Export configuration as string (JSON or JS).
import { exportConfig } from '@happyvertical/smrt-config';
const config = { /* your config */ };
// Export as JSON (secrets removed by default)
const jsonStr = exportConfig(config);
// Export as JS module with secrets
const jsStr = exportConfig(config, {
includeSecrets: true,
format: 'js',
indent: 2
});Environment Variables
Use SMRT_ prefix with double underscore for nesting:
# Global config
SMRT_LOG_LEVEL=debug
SMRT_CACHE_DIR=/tmp/cache
SMRT_ENVIRONMENT=production
# Package config
SMRT_AI__DEFAULT_MODEL=gpt-4-turbo
SMRT_AI__TEMPERATURE=0.8
# Module config
SMRT_MODULES__MY_SCRAPER__ENABLED=true
SMRT_MODULES__MY_SCRAPER__MAX_PAGES=500Tutorials
Tutorial 1: Multi-Environment Configuration
Configure your application for different environments.
Step 1: Create base configuration
// smrt.config.js
export default {
smrt: {
logLevel: process.env.NODE_ENV === 'production' ? 'warn' : 'debug',
cacheDir: '.cache'
},
packages: {
ai: {
defaultProvider: 'anthropic',
defaultModel: 'claude-3-5-sonnet-20241022'
}
}
};Step 2: Environment-specific overrides
# Development (.env.development)
SMRT_LOG_LEVEL=debug
SMRT_AI__DEFAULT_MODEL=claude-3-haiku-20240307
# Production (.env.production)
SMRT_LOG_LEVEL=error
SMRT_AI__DEFAULT_MODEL=claude-3-5-sonnet-20241022
SMRT_CACHE_DIR=/var/cache/smrtStep 3: Load and verify
import { loadConfig, getPackageConfig } from '@happyvertical/smrt-config';
await loadConfig();
const aiConfig = getPackageConfig('ai');
console.log(`Running with ${aiConfig.defaultModel}`);
// Development: claude-3-haiku-20240307
// Production: claude-3-5-sonnet-20241022Tutorial 2: Remote Configuration Loading
Load configuration from a remote API at startup using top-level await.
Step 1: Fetch remote config
// smrt.config.js
const remoteConfig = await fetch('https://api.example.com/config')
.then(res => res.json())
.catch(err => {
console.error('Failed to load remote config:', err);
return {}; // Fallback to empty config
});
export default {
smrt: {
logLevel: remoteConfig.logLevel || 'info',
...remoteConfig.global
},
packages: remoteConfig.packages || {},
modules: remoteConfig.modules || {}
};Step 2: Merge with local overrides
// smrt.config.js
const remoteConfig = await fetch('https://api.example.com/config')
.then(res => res.json())
.catch(() => ({}));
export default {
smrt: {
...remoteConfig.global,
// Local overrides take precedence
cacheDir: './.cache',
logLevel: process.env.LOG_LEVEL || remoteConfig.logLevel || 'info'
},
packages: {
...remoteConfig.packages,
// Keep secrets local, never remote
ai: {
...remoteConfig.packages?.ai,
apiKey: process.env.ANTHROPIC_API_KEY
}
}
};Tutorial 3: Testing with Configuration
Use runtime configuration for test-specific settings.
import { beforeEach, afterEach, test } from 'vitest';
import { setConfig, clearCache, getPackageConfig } from '@happyvertical/smrt-config';
beforeEach(() => {
// Set test-specific config
setConfig({
packages: {
ai: {
defaultProvider: 'mock',
defaultModel: 'test-model'
}
}
});
});
afterEach(() => {
// Clear cache between tests
clearCache();
});
test('uses test configuration', () => {
const config = getPackageConfig('ai');
expect(config.defaultProvider).toBe('mock');
});Integration with SMRT Modules
smrt-core Integration
Configure core framework behavior:
// smrt.config.js
export default {
smrt: {
// Schema migration strategy
schemaMigration: 'auto-add', // or 'warn'
// Inheritance cache for performance
inheritance: {
cacheSize: 1000,
ttl: 3600
},
// Embedding provider
embeddings: {
provider: 'local', // 'local' | 'ai' | 'auto'
model: 'all-MiniLM-L6-v2'
}
}
};Custom Package Integration
Use configuration in your own packages:
// In your package
import { getPackageConfig } from '@happyvertical/smrt-config';
interface MyPackageConfig {
enabled: boolean;
timeout: number;
retries: number;
}
export function initializeMyPackage() {
const config = getPackageConfig<MyPackageConfig>('my-package', {
enabled: true,
timeout: 30000,
retries: 3
});
if (!config.enabled) {
console.log('Package is disabled');
return;
}
// Use config values...
}Best Practices
✅ DO
- Keep secrets in environment variables, never hardcoded in config files
- Use sensible defaults in
getPackageConfig()/getModuleConfig() - Validate remote configurations before using them
- Cache remote configs with appropriate TTL for performance
- Use
defineConfig()for TypeScript type safety - Handle fetch errors gracefully with fallback configurations
- Test configuration loading in CI/CD pipeline
- Document expected configuration structure for your modules
❌ DON'T
- Hardcode API keys or secrets in configuration files
- Call
loadConfig()multiple times unnecessarily - Store sensitive data in version control
- Share credentials across different environments
- Rely on undocumented internal configuration structure
- Mix configuration scopes incorrectly (use appropriate scope)
- Forget to call
clearCache()between tests
Troubleshooting
Config file not found
Problem: Configuration file is not being detected.
Solution: Ensure smrt.config.* is in your project root, or specify configPath explicitly:
await loadConfig({ configPath: './config/smrt.config.js' });Environment variables not working
Problem: Environment variables are not being applied.
Solution: Verify you're using the SMRT_ prefix and double underscore
for nesting:
# Correct
SMRT_AI__DEFAULT_MODEL=gpt-4
# Incorrect (missing SMRT_ prefix)
AI__DEFAULT_MODEL=gpt-4TypeScript errors in config file
Problem: Getting type errors in smrt.config.ts.
Solution: Use the defineConfig() helper:
import { defineConfig } from '@happyvertical/smrt-config';
export default defineConfig({
// Full type safety and autocomplete here
smrt: {
logLevel: 'info' // TypeScript will validate this
}
});Config not shared across packages in monorepo
Problem: Different packages see different configurations.
Solution: Ensure loadConfig() is called early in your application
lifecycle. The package uses globalThis caching to share config across all package instances.
API Quick Reference
| Function | Purpose | Returns |
|---|---|---|
loadConfig(options?) | Load config from file | Promise<SmrtConfig> |
getConfig() | Get cached config | SmrtConfig | null |
getModuleConfig(name, defaults?) | Get module config | T |
getPackageConfig(name, defaults?) | Get package config | T |
getSiteConfig() | Get site identity | SiteConfig | null |
setConfig(config) | Set runtime config | void |
clearCache() | Clear all caches | void |
defineConfig(config) | Type-safe helper | SmrtConfig |
sanitizeConfig(config) | Remove secrets | unknown |
exportConfig(config, opts?) | Export to string | string |