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:
- Environment variables — Always win. Ideal for CI/CD and per-machine overrides.
- Inline values — Passed programmatically via the
valuesoption. Useful for runtime overrides. - JSON config file — Loaded from disk via the
fileoption. Good for project-level defaults. - 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.