Configuration

Type-safe configuration management with Zod validation

The @justscale/config package provides type-safe configuration management with Zod schema validation, environment variable support, profile management, and runtime mutations.

Installation

Bash
pnpm add @justscale/config

Defining Config Partials

Use defineConfigPartial to create type-safe configuration sections with Zod schemas:

config/database.tsTypeScript
import { defineConfigPartial } from '@justscale/config';
import { z } from 'zod';

// Define a config partial with a Zod schema
export const DatabaseConfig = defineConfigPartial('database', z.object({
  host: z.string().default('localhost'),
  port: z.number().default(5432),
  database: z.string(),
  username: z.string(),
  password: z.string(),
  poolSize: z.number().min(1).max(100).default(10),
}));

// Type is inferred from the schema
type DatabaseConfigType = z.infer<typeof DatabaseConfig.schema>;
// { host: string; port: number; database: string; ... }

Injecting Configuration

Use Config.of() to create injection tokens for config partials:

services/database.service.tsTypeScript
import { createService } from '@justscale/core';
import { Config } from '@justscale/config';
import { DatabaseConfig } from '../config/database';

export const DatabaseService = createService({
  inject: {
    // Config.of() creates a type-safe injection token
    config: Config.of(DatabaseConfig),
  },
  factory: ({ config }) => {
    // config is fully typed based on the Zod schema
    const connectionString = `postgres://${config.username}:${config.password}@${config.host}:${config.port}/${config.database}`;

    return {
      getConnectionString: () => connectionString,
      getPoolSize: () => config.poolSize,
    };
  },
});

Providing Configuration

Use createConfig to provide config values at application startup:

main.tsTypeScript
import { createClusterBuilder } from '@justscale/core';
import { AppConfig } from './config';
import { DatabaseService } from './services/database.service';

const cluster = createClusterBuilder()
  .add(AppConfig)           // Provide configuration
  .add(DatabaseService)     // Services can now inject config
  .build();

await cluster.compile();
await cluster.start();

Environment Variables

The EnvServiceDef provides type-safe access to environment variables:

services/example.service.tsTypeScript
import { createService } from '@justscale/core';
import { EnvServiceDef } from '@justscale/config';

export const ExampleService = createService({
  inject: {
    env: EnvServiceDef,
  },
  factory: ({ env }) => ({
    getApiKey: () => env.get('API_KEY'),
    getOptionalValue: () => env.get('OPTIONAL_VAR', 'default-value'),
    isProduction: () => env.get('NODE_ENV') === 'production',
  }),
});

Profile-Based Configuration

Use profiles to manage different configuration sets for development, staging, and production environments:

config/profiles.tsTypeScript
import { createConfig } from '@justscale/config';
import { DatabaseConfig } from './database';

// Development profile
export const DevConfig = createConfig({
  factory: () => ({
    [DatabaseConfig.key]: {
      host: 'localhost',
      port: 5432,
      database: 'myapp_dev',
      username: 'dev',
      password: 'dev',
      poolSize: 5,
    },
  }),
});

// Production profile
export const ProdConfig = createConfig({
  factory: () => ({
    [DatabaseConfig.key]: {
      host: process.env.DB_HOST!,
      port: parseInt(process.env.DB_PORT!),
      database: process.env.DB_NAME!,
      username: process.env.DB_USER!,
      password: process.env.DB_PASS!,
      poolSize: 50,
    },
  }),
});

Runtime Mutations

The ConfigServiceDef allows runtime configuration updates and watching for changes:

services/settings.service.tsTypeScript
import { createService } from '@justscale/core';
import { ConfigServiceDef, Config } from '@justscale/config';
import { AppSettings } from '../config/settings';

export const SettingsService = createService({
  inject: {
    configService: ConfigServiceDef,
    settings: Config.of(AppSettings),
  },
  factory: ({ configService, settings }) => ({
    // Get current value
    getDebugMode: () => settings.debugMode,

    // Update config at runtime
    setDebugMode: async (enabled: boolean) => {
      configService.set(AppSettings, 'debugMode', enabled);
    },

    // Watch for config changes
    watchSettings: async function* () {
      for await (const [oldConfig, newConfig] of configService.watch(AppSettings)) {
        yield { old: oldConfig, new: newConfig };
      }
    },
  }),
});

Runtime mutations are persisted to .justscale/config.json and survive application restarts.

CLI Integration

Add configuration CLI commands to manage settings from the command line:

terminal.shTypeScript
# View all config
justscale config list

# Get a specific value
justscale config get database.host

# Set a value at runtime
justscale config set database.poolSize 25

# View config as JSON
justscale config dump

Profile Service

The ProfileServiceDef provides file-based profile management for switching between configuration sets (local, dev, staging, production):

services/deploy.service.tsTypeScript
import { createService } from '@justscale/core';
import { ProfileServiceDef } from '@justscale/config';

export const DeployService = createService({
  inject: {
    profiles: ProfileServiceDef,
  },
  factory: ({ profiles }) => ({
    // Get active profile (checks JUSTSCALE_PROFILE env, then .justscale/.active-profile)
    getCurrentProfile: () => profiles.active(),

    // Switch to a different profile
    switchProfile: (name: string) => {
      profiles.use(name);  // Throws if profile doesn't exist
    },

    // List all available profiles
    listProfiles: () => profiles.list(),

    // Create a new profile (optionally copy from existing)
    createProfile: (name: string, copyFrom?: string) => {
      profiles.create(name, copyFrom);
    },

    // Compare two profiles
    diffProfiles: (from: string, to: string) => {
      return profiles.diff(from, to);
      // Returns: [{ key: 'database.host', from: 'localhost', to: 'prod-db' }, ...]
    },
  }),
});

Profile Priority

The active profile is determined by (in order):

  • JUSTSCALE_PROFILE environment variable
  • .justscale/.active-profile file
  • Default: local

Profile CLI Commands

terminal.shTypeScript
# List available profiles
justscale profile list

# Switch to a profile
justscale profile use staging

# Create a new profile (copy from existing)
justscale profile create prod --from staging

# Compare profiles
justscale profile diff staging prod

Validation

All configuration is validated against the Zod schema at startup and during runtime mutations:

config/api.tsTypeScript
import { defineConfigPartial } from '@justscale/config';
import { z } from 'zod';

export const ApiConfig = defineConfigPartial('api', z.object({
  // Required fields
  baseUrl: z.string().url(),
  apiKey: z.string().min(32),

  // Optional with defaults
  timeout: z.number().positive().default(30000),
  retries: z.number().int().min(0).max(10).default(3),

  // Enum values
  logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
}));

If validation fails, an error is thrown with detailed information about which fields failed and why.

Best Practices

  • Group related settings - Create separate config partials for each concern (database, redis, api, etc.)
  • Use sensible defaults - Provide default values in your Zod schemas for development convenience
  • Validate early - Configuration is validated at startup, failing fast if values are invalid
  • Keep secrets out of code - Use environment variables for sensitive values like passwords and API keys
  • Use profiles - Create separate config components for different environments to avoid conditional logic