@have/config Specification
Overview
@have/config provides centralized configuration management for SMRT modules and applications. It allows module options to be declared throughout the codebase while maintaining a single source of truth in project-level configuration files.
Core Concepts
Configuration File Formats
Support multiple configuration file formats with automatic detection:
smrt.config.js- JavaScript (ESM)smrt.config.ts- TypeScriptsmrt.config.json- JSONsmrt.config.yaml/smrt.config.yml- YAMLsmrt.config.toml- TOML
Configuration Structure
interface SmrtConfig {
// Global SMRT framework options
smrt?: {
cacheDir?: string;
logLevel?: 'debug' | 'info' | 'warn' | 'error';
environment?: 'development' | 'production' | 'test';
};
// Module-scoped configurations
modules?: {
[moduleName: string]: Record<string, unknown>;
};
// Package-scoped configurations
packages?: {
ai?: AIConfig;
cache?: CacheConfig;
spider?: SpiderConfig;
sql?: SQLConfig;
[packageName: string]: Record<string, unknown>;
};
}
API Design
Core Functions
/**
* Load and parse configuration from project root
*/
export async function loadConfig(
options?: LoadConfigOptions
): Promise<SmrtConfig>;
/**
* Get configuration for a specific module
*/
export function getModuleConfig<T = unknown>(
moduleName: string,
defaults?: T
): T;
/**
* Get configuration for a specific package
*/
export function getPackageConfig<T = unknown>(
packageName: string,
defaults?: T
): T;
/**
* Set configuration at runtime (merged with file config)
*/
export function setConfig(config: Partial<SmrtConfig>): void;
/**
* Watch configuration file for changes
*/
export function watchConfig(
callback: (config: SmrtConfig) => void
): () => void;
/**
* Validate configuration against schema
*/
export function validateConfig(
config: unknown,
schema?: ConfigSchema
): ValidationResult;
Options Types
interface LoadConfigOptions {
// Custom config file path (default: auto-detect in cwd)
configPath?: string;
// Search parent directories (default: true)
searchParents?: boolean;
// Merge with environment variables (default: true)
useEnv?: boolean;
// Environment variable prefix (default: 'SMRT_')
envPrefix?: string;
// Validate against schemas (default: true)
validate?: boolean;
// Cache loaded config (default: true)
cache?: boolean;
}
interface ValidationResult {
valid: boolean;
errors?: ValidationError[];
warnings?: ValidationWarning[];
}
Implementation Details
File Discovery
- Start from current working directory
- Look for
smrt.config.*in order:.ts,.js,.json,.yaml,.yml,.toml - If
searchParents: true, traverse up directory tree - Stop at first config file found or project root (contains
package.json)
Configuration Merging
Priority order (highest to lowest):
- Runtime config set via
setConfig() - Environment variables (with prefix)
- Configuration file
- Module/package defaults
Environment Variable Mapping
# Global options
SMRT_CACHE_DIR=/tmp/cache
SMRT_LOG_LEVEL=debug
SMRT_ENVIRONMENT=production
# Package-scoped options (double underscore separator)
SMRT_AI__DEFAULT_MODEL=gpt-4
SMRT_CACHE__PROVIDER=redis
SMRT_SPIDER__HEADLESS=true
# Module-scoped options
SMRT_MODULES__MY_MODULE__OPTION=value
TypeScript Integration
// Auto-generate types from config
import type { defineConfig } from '@have/config';
export default defineConfig({
smrt: {
cacheDir: '.cache',
logLevel: 'info',
},
packages: {
ai: {
defaultProvider: 'openai',
defaultModel: 'gpt-4',
},
spider: {
headless: true,
cacheDir: '.cache/spider',
},
},
modules: {
'my-custom-module': {
apiKey: process.env.CUSTOM_API_KEY,
timeout: 30000,
},
},
});
Schema Validation
// Packages can export config schemas
export const aiConfigSchema = {
type: 'object',
properties: {
defaultProvider: {
type: 'string',
enum: ['openai', 'anthropic', 'google', 'aws']
},
defaultModel: { type: 'string' },
apiKeys: {
type: 'object',
additionalProperties: { type: 'string' }
},
},
};
// Validate at load time
const config = await loadConfig({ validate: true });
// Throws if invalid
Usage Examples
Basic Configuration File
// smrt.config.js
export default {
smrt: {
cacheDir: '.cache',
logLevel: 'info',
},
packages: {
ai: {
defaultProvider: 'anthropic',
defaultModel: 'claude-3-5-sonnet-20241022',
apiKeys: {
anthropic: process.env.ANTHROPIC_API_KEY,
},
},
spider: {
headless: true,
cacheDir: '.cache/spider',
},
cache: {
provider: 'file',
cacheDir: '.cache',
},
},
modules: {
'town-scraper': {
cronSchedule: '0 0 * * *',
maxPages: 100,
},
},
};
Remote Configuration with Top-Level Await
// smrt.config.js - Load from remote source
const remoteConfig = await fetch('https://config.example.com/smrt.json')
.then(r => r.json());
// Merge with local overrides
export default {
...remoteConfig,
// Override specific values locally
smrt: {
...remoteConfig.smrt,
logLevel: 'debug', // Local override
},
packages: {
...remoteConfig.packages,
ai: {
...remoteConfig.packages.ai,
apiKeys: {
// Keep secrets local, never from remote
anthropic: process.env.ANTHROPIC_API_KEY,
},
},
},
};
Dynamic Configuration Loading
// smrt.config.js - Load from multiple sources
const [baseConfig, featureFlags] = await Promise.all([
fetch('https://config.cdn.com/base.json').then(r => r.json()),
fetch('https://flags.example.com/features').then(r => r.json()),
]);
export default {
...baseConfig,
features: featureFlags,
packages: {
ai: {
defaultModel: featureFlags.useGPT4 ? 'gpt-4' : 'gpt-3.5-turbo',
},
},
};
Using Config in Code
import { getPackageConfig, getModuleConfig } from '@have/config';
// In a package
export async function createAIClient() {
const config = getPackageConfig<AIConfig>('ai', {
defaultProvider: 'openai',
defaultModel: 'gpt-4',
});
return getAI({
provider: config.defaultProvider,
model: config.defaultModel,
apiKey: config.apiKeys?.[config.defaultProvider],
});
}
// In a SMRT module
@smrt()
export class TownScraper {
private config = getModuleConfig('town-scraper', {
cronSchedule: '0 0 * * *',
maxPages: 50,
});
async scrape() {
console.log(`Running with schedule: ${this.config.cronSchedule}`);
}
}
Runtime Configuration
import { setConfig, watchConfig } from '@have/config';
// Override config at runtime
setConfig({
packages: {
ai: {
defaultModel: 'gpt-4-turbo',
},
},
});
// Watch for config changes (useful in development)
const unwatch = watchConfig((newConfig) => {
console.log('Config updated:', newConfig);
});
// Later...
unwatch();
Dependencies
cosmiconfig- Configuration file discovery and loadingzod- Schema validationyaml- YAML parsingtoml- TOML parsingchokidar- File watchingdot-prop- Deep object property access
Package Structure
@have/config/
├── src/
│ ├── index.ts # Main exports
│ ├── loader.ts # Config file loading
│ ├── merge.ts # Config merging logic
│ ├── env.ts # Environment variable parsing
│ ├── validate.ts # Schema validation
│ ├── watch.ts # File watching
│ └── types.ts # TypeScript types
├── package.json
├── tsconfig.json
├── README.md
└── SPEC.md (this file)
Testing Strategy
-
Unit Tests
- Config loading from different formats
- Environment variable parsing
- Configuration merging
- Schema validation
-
Integration Tests
- End-to-end config loading
- Package integration examples
- File watching
-
Edge Cases
- Missing config files (use defaults)
- Invalid config syntax
- Circular dependencies
- Concurrent file changes
Advanced Orchestration Patterns
With top-level await, config files become powerful orchestration tools:
Multi-Source Configuration Aggregation
// smrt.config.js - Aggregate from multiple sources
const [
infraConfig, // Infrastructure settings from Terraform outputs
secretsConfig, // Secrets from Vault/AWS Secrets Manager
featureFlags, // Feature flags from LaunchDarkly/Split
teamSettings, // Team preferences from internal API
] = await Promise.all([
fetch('https://infra.company.com/outputs').then(r => r.json()),
fetch('https://vault.company.com/secrets/app').then(r => r.json()),
fetch('https://flags.company.com/features').then(r => r.json()),
fetch('https://api.company.com/team/settings').then(r => r.json()),
]);
export default {
smrt: {
environment: infraConfig.environment,
logLevel: teamSettings.defaultLogLevel,
},
packages: {
ai: {
defaultModel: featureFlags.useGPT4 ? 'gpt-4' : 'gpt-3.5-turbo',
apiKeys: secretsConfig.aiKeys,
},
sql: {
connectionString: infraConfig.databaseUrl,
maxConnections: infraConfig.dbPoolSize,
},
},
};
Conditional Module Loading
// smrt.config.js - Load modules based on environment
const environment = process.env.NODE_ENV;
const productionModules = environment === 'production'
? await fetch('https://cdn.company.com/prod-modules.json').then(r => r.json())
: {};
export default {
modules: {
...productionModules,
// Development-only modules
...(environment === 'development' && {
'debug-tools': {
enabled: true,
port: 9229,
},
}),
},
};
Service Discovery Integration
// smrt.config.js - Discover services dynamically
const serviceRegistry = await fetch('https://consul.internal/v1/catalog/services')
.then(r => r.json());
const aiService = await fetch('https://consul.internal/v1/health/service/ai-gateway')
.then(r => r.json())
.then(services => services[0]); // Get first healthy instance
export default {
packages: {
ai: {
baseUrl: `http://${aiService.Service.Address}:${aiService.Service.Port}`,
timeout: 30000,
},
},
};
Configuration Validation Against Remote Schemas
// smrt.config.js - Validate against remote JSON Schema
const configSchema = await fetch('https://schemas.company.com/smrt-config.schema.json')
.then(r => r.json());
const localConfig = {
packages: {
ai: {
defaultProvider: 'anthropic',
defaultModel: 'claude-3-5-sonnet-20241022',
},
},
};
// Validate before export
const { default: Ajv } = await import('ajv');
const ajv = new Ajv();
const validate = ajv.compile(configSchema);
if (!validate(localConfig)) {
throw new Error(`Invalid config: ${JSON.stringify(validate.errors)}`);
}
export default localConfig;
Gnode Federation Discovery
// smrt.config.js - Discover and federate with other gnodes
const nearbyGnodes = await fetch('https://registry.gnodes.network/nearby?lat=52.52&lon=13.40&radius=50')
.then(r => r.json());
export default {
modules: {
'gnode-federation': {
peers: nearbyGnodes.map(node => ({
id: node.id,
name: node.name,
url: node.apiUrl,
topics: node.topics,
})),
syncInterval: 3600000, // 1 hour
},
},
packages: {
spider: {
allowedDomains: nearbyGnodes.flatMap(node => node.domains),
},
},
};
Dynamic Model Selection Based on Cost/Availability
// smrt.config.js - Smart model selection
const [pricing, availability] = await Promise.all([
fetch('https://api.company.com/ai/pricing').then(r => r.json()),
fetch('https://status.openai.com/api/v2/status.json').then(r => r.json()),
]);
const selectModel = () => {
if (availability.status.indicator === 'major') {
return 'claude-3-5-sonnet-20241022'; // Fallback to Anthropic
}
const budget = process.env.AI_BUDGET || 100;
return pricing.gpt4Price < budget ? 'gpt-4' : 'gpt-3.5-turbo';
};
export default {
packages: {
ai: {
defaultModel: selectModel(),
fallbackModels: ['claude-3-5-sonnet-20241022', 'gpt-3.5-turbo'],
},
},
};
Secrets Management with Async Decryption
// smrt.config.js - Decrypt secrets at load time
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
const client = new SecretManagerServiceClient();
const [anthropicKey, openaiKey] = await Promise.all([
client.accessSecretVersion({ name: 'projects/my-project/secrets/anthropic-key/versions/latest' })
.then(([version]) => version.payload.data.toString()),
client.accessSecretVersion({ name: 'projects/my-project/secrets/openai-key/versions/latest' })
.then(([version]) => version.payload.data.toString()),
]);
export default {
packages: {
ai: {
apiKeys: {
anthropic: anthropicKey,
openai: openaiKey,
},
},
},
};
Git-Based Configuration Versioning
// smrt.config.js - Load config from specific git commit
import { simpleGit } from 'simple-git';
const git = simpleGit();
const deployCommit = process.env.DEPLOY_COMMIT || 'HEAD';
// Checkout config from specific commit
const configContent = await git.show([`${deployCommit}:config/production.json`]);
const baseConfig = JSON.parse(configContent);
export default {
...baseConfig,
meta: {
deployedAt: new Date().toISOString(),
gitCommit: await git.revparse(['HEAD']),
gitBranch: await git.revparse(['--abbrev-ref', 'HEAD']),
},
};
Future Enhancements
- Config Encryption - Encrypt sensitive values at rest
- Config Profiles - Support development/staging/production profiles
- Config Caching - Cache remote configs with TTL
- Config UI - Web-based configuration editor
- Config Migration - Auto-migrate old config formats
- Config Diff - Show differences between environments
- Config Rollback - Version history and rollback support