Configuration System

Overview

BitDiver uses Zod schemas for configuration with full TypeScript type inference. The loadConfig() function loads and validates configuration from multiple sources, merging them according to a well-defined priority order. This means you can define sensible defaults in your schema, override them with a config file, pass one-off values programmatically, and let environment variables take final precedence—all with end-to-end type safety.

Priority System

When the same key is defined in multiple sources, the highest-priority source wins. From highest to lowest:

  1. Environment variables — Always win. Ideal for CI/CD and per-machine overrides.
  2. Inline values — Passed programmatically via the values option. Useful for runtime overrides.
  3. JSON config file — Loaded from disk via the file option. Good for project-level defaults.
  4. Schema defaults — Defined in the Zod schema via .default(). The fallback when nothing else is set.

Defining a Schema

Define your configuration shape as a Zod object. Every field gets full type inference automatically. Use .default() for optional values and leave required fields without a default so that validation catches missing configuration early.

import { z } from 'zod'
import { loadConfig } from '@xhubio/bitdiver-runner'

const schema = z.object({
  database: z.object({
    host: z.string().default('localhost'),
    port: z.number().default(5432),
    name: z.string()
  }),
  timeout: z.number().default(30000),
  apiKey: z.string()
})

In this example database.host, database.port, and timeout have defaults, while database.name and apiKey are required—loading will fail if they are not provided by at least one source.

Loading Configuration

Call loadConfig() with your schema and the sources you want to merge. The returned result.config object is fully typed according to your schema.

const result = await loadConfig({
  schema,
  file: './config.json',
  values: { timeout: 60000 },
  envPrefix: 'MYAPP',
  secrets: ['apiKey', 'database.password']
})

const config = result.config
// config.database.host → typed as string
// config.timeout → 60000 (inline value wins over file/default)

Environment Variables

Environment variables are mapped to configuration paths using a simple naming convention. The envPrefix you provide is prepended, and the nested path is converted to UPPER_SNAKE_CASE:

ENVPREFIX_PATH_IN_UPPER_SNAKE

Examples (envPrefix: 'MYAPP'):
  database.host     →  MYAPP_DATABASE_HOST
  database.port     →  MYAPP_DATABASE_PORT
  apiKey            →  MYAPP_API_KEY
  timeout           →  MYAPP_TIMEOUT

Because environment variables always have the highest priority, setting MYAPP_DATABASE_HOST=prod-db.example.com will override any value from the config file, inline values, or schema defaults.

Secret Masking

Fields listed in the secrets array are masked when you call result.toString(). This lets you safely log the resolved configuration without exposing sensitive values like API keys or passwords.

const result = await loadConfig({
  schema,
  file: './config.json',
  envPrefix: 'MYAPP',
  secrets: ['apiKey', 'database.password']
})

console.log(result.toString())
// database.host: localhost
// database.port: 5432
// database.name: mydb
// apiKey: ******
// database.password: ******

The actual values are still available on result.config—only the string representation is masked.

Using Config in Steps (StepSetupConfig)

In a real test suite you typically load configuration during the setup phase so it is available to all subsequent steps. StepSetupConfig is a built-in step that loads configuration into EnvironmentRun, making it accessible from any step via this.environment.

import { StepSetupConfig } from '@xhubio/bitdiver-runner'

class SetupConfig extends StepSetupConfig {
  schema = myConfigSchema
  configFile = './config.json'
  envPrefix = 'MYAPP'
  secrets = ['apiKey']
}

Register this step and place it first in your suite’s step array. When the runner executes it, the validated configuration object is stored on the run environment and can be retrieved by any step that follows.