s-m-r-t

@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.

v0.19.0Core FoundationESM

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

typescript
npm install @happyvertical/smrt-config
# or
pnpm add @happyvertical/smrt-config
# or
bun add @happyvertical/smrt-config

Quick Start (5 Minutes)

1. Create Configuration File

Create smrt.config.js in your project root:

typescript
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:

typescript
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:

typescript
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):

  1. Runtime config - Set via setConfig()
  2. Environment variables - SMRT_* prefix
  3. File-based config - smrt.config.* files
  4. 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.

typescript
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.

typescript
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.

typescript
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 config

getPackageConfig<T>(packageName, defaults?): T

Get package-specific configuration with defaults.

typescript
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).

typescript
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.

typescript
import { clearCache } from '@happyvertical/smrt-config';

// Useful for testing or hot-reload
clearCache();

defineConfig(config): SmrtConfig

Helper for type-safe configuration in TypeScript.

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.

typescript
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).

typescript
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:

typescript
# 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=500

Tutorials

Tutorial 1: Multi-Environment Configuration

Configure your application for different environments.

Step 1: Create base configuration

typescript
// 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

typescript
# 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/smrt

Step 3: Load and verify

typescript
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-20241022

Tutorial 2: Remote Configuration Loading

Load configuration from a remote API at startup using top-level await.

Step 1: Fetch remote config

typescript
// 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

typescript
// 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.

typescript
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:

typescript
// 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:

typescript
// 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:

typescript
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:

typescript
# Correct
SMRT_AI__DEFAULT_MODEL=gpt-4

# Incorrect (missing SMRT_ prefix)
AI__DEFAULT_MODEL=gpt-4

TypeScript errors in config file

Problem: Getting type errors in smrt.config.ts.

Solution: Use the defineConfig() helper:

typescript
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

FunctionPurposeReturns
loadConfig(options?)Load config from filePromise<SmrtConfig>
getConfig()Get cached configSmrtConfig | null
getModuleConfig(name, defaults?)Get module configT
getPackageConfig(name, defaults?)Get package configT
getSiteConfig()Get site identitySiteConfig | null
setConfig(config)Set runtime configvoid
clearCache()Clear all cachesvoid
defineConfig(config)Type-safe helperSmrtConfig
sanitizeConfig(config)Remove secretsunknown
exportConfig(config, opts?)Export to stringstring