Docs
/
Packages
/
@zudojs/config
v1.3.1
@zudojs/config
Layered configuration system for Zudo — environment variables, in-memory and custom (e.g. remote) sources, runtime overrides, type-safe schemas, and sensitive value redaction
CONFIGURATION
ENV VARS
REDACTION
OVERVIEW
@zudojs/config provides a complete, layered configuration system for Zudo applications. It handles loading configuration from multiple sources (environment variables, files, memory, remote), storing values in a reactive in-memory store, resolving typed values with schema validation, and managing the full configuration lifecycle.
// The configuration pipeline
Source → Store → Resolver → Manager
// Sources load raw key-value pairs
// Store holds entries with metadata
// Resolver provides typed, validated access
// Manager orchestrates the full lifecycle
Multi-Source Loading
Load from environment, files, memory, or remote sources with priority-based resolution.
Type-Safe Access
Typed resolver methods for string, number, boolean, date, object, and array values.
Schema Validation
Define schemas with types, constraints, defaults, and custom validators.
Lifecycle Management
State machine: CREATED → LOADING → READY → RELOADING → FAILED → DISPOSED.
INSTALLATION
// npm
npm install @zudojs/config
// pnpm
pnpm add @zudojs/config
// yarn
yarn add @zudojs/config
Dependency: @zudojs/config 1.3.1 depends on @zudojs/errors 1.3.0 and @zudojs/constants 1.1.2 (exact versions, installed for you). It uses ConfigurationError and related error factories from @zudojs/errors.
ARCHITECTURE
The config package follows a layered architecture where each layer has a single responsibility:
// Layered architecture
configFactory // Convenience facade
↓
configManager // Lifecycle orchestration
├── configLoader // Loads sources into store
│ └── configSource // Source abstractions + factories
├── configStore // In-memory key-value with events
│ └── configEntry // Typed entry with metadata
└── configResolver // Typed access + validation
├── configSchema // Schema types + validation
├── ScopedResolver // Prefix-scoped sub-resolver
└── accessors // Standalone typed accessors
Design Principle: Each layer depends only on layers below it. The factory depends on the manager, which depends on loader/store/resolver. The resolver depends on the store and schema. This ensures clean separation of concerns.
QUICK START
Create and Load Configuration
import { createConfigManager } from '@zudojs/config';
const manager = createConfigManager({
initialValues: {
'app.name': 'My App',
'app.port': 3000,
'app.debug': true,
'db.host': 'localhost',
'db.port': 5432,
}
});
await manager.load();
// Type-safe access
const name = manager.string('app.name'); // 'My App'
const port = manager.number('app.port'); // 3000
const debug = manager.boolean('app.debug'); // true
Using ConfigFactory (Shorthand)
import { configFactory, ConfigValueType } from '@zudojs/config';
const manager = await configFactory.initialize({
initialValues: {
'app.name': 'My App',
'app.port': 3000,
}
});
// Or create a validated configuration. `validated` takes the object
// schema itself — its `properties` map — not a wrapper with a `schema` key.
// Options go in the second argument; without values, the required
// 'app.name' is missing and validated() throws ConfigManagerValidationError.
const { manager: validatedManager, config } = await configFactory.validated(
{
properties: {
'app.name': { type: ConfigValueType.STRING, required: true },
'app.port': { type: ConfigValueType.NUMBER, min: 1, max: 65535 },
},
},
{ initialValues: { 'app.name': 'My App', 'app.port': 3000 } },
);
Use the ConfigValueType enum for type. A bare string literal such as type: 'string' widens to string in a plain object and will not satisfy ConfigSchema. ConfigValueType.STRING is the value "string", so the runtime shape is identical — it just keeps the literal type intact.
Scoped Resolvers
const db = manager.scoped('db');
// All keys are automatically prefixed
const host = db.string('host'); // reads 'db.host'
const port = db.number('port'); // reads 'db.port'
const name = db.string('name'); // reads 'db.name'
// Typed required accessors parse, check and throw (on scoped resolvers since v1.3.0)
const dbPort = db.requiredNumber('port'); // DB__PORT=5432 gives 5432 (a number)
CONFIG VALUE
The foundation types and utilities for configuration values.
Types
// Primitives that can be config values
type ConfigPrimitive = string | number | boolean | bigint | null | undefined;
// JSON-compatible recursive type
type ConfigJsonValue = string | number | boolean | null
| ConfigJsonValue[] | { [key: string]: ConfigJsonValue };
// Full config value (includes Date and recursive structures)
type ConfigValue = ConfigPrimitive | Date
| ConfigValue[] | { [key: string]: ConfigValue };
Type Guards
import { isConfigPrimitive, isConfigObject, isConfigValue } from '@zudojs/config';
isConfigPrimitive('hello'); // true
isConfigPrimitive(42); // true
isConfigPrimitive([]); // false
isConfigObject({ a: 1 }); // true
isConfigObject([1, 2]); // false (arrays are not objects)
isConfigObject(new Date()); // false
isConfigValue('hello'); // true
isConfigValue({ a: [1, 2] }); // true (recursive)
Parsers
import {
parseConfigString, parseConfigBoolean,
parseConfigNumber, parseConfigDate
} from '@zudojs/config';
parseConfigBoolean('true'); // true
parseConfigBoolean('yes'); // true
parseConfigBoolean('1'); // true
parseConfigBoolean('no'); // false
parseConfigNumber('42'); // 42
parseConfigNumber('3.14'); // 3.14
parseConfigDate('2026-01-01'); // Date object
Utilities
import { freezeConfigValue, cloneConfigValue, configValuesEqual } from '@zudojs/config';
const frozen = freezeConfigValue({ nested: { value: 42 } });
// Deep freeze — all levels are immutable
const cloned = cloneConfigValue(frozen);
// Deep clone — Date objects are cloned via new Date()
configValuesEqual({ a: 1 }, { a: 1 }); // true
configValuesEqual({ a: 1 }, { a: 2 }); // false
CONFIG ENTRY
Configuration entries wrap values with provenance metadata — tracking where each value came from, its priority, and whether it's sensitive.
Keys and values are screened (isSensitiveConfigEntry): password/secret/token/api-key/private-key/credential/auth/dsn/database-url/connection-string/*_key names in any segment, nested objects containing them, and URLs with user:password@ are marked sensitive and redacted.
Changed in v1.2.0 — screening now covers every write path. Detection runs inside ConfigStore.set(), so a key such as db.password, api_key or a postgres://user:pw@host connection string is marked sensitive however it was written: from a source, from initialValues, from set() / setMany() / replace(), or from manager.set(). Before v1.2.0 only values arriving through a source were screened, so toSafeObject() printed the identical key in clear when it had been seeded or set at runtime. If you relied on that, pass sensitive: false explicitly to opt a key out.
Interface
interface ConfigEntry<T extends ConfigValue = ConfigValue> {
readonly key: string;
readonly value: T;
readonly source: string;
readonly sourceType: ConfigSourceType;
readonly priority: number;
readonly sensitive: boolean;
readonly resolved: boolean;
readonly createdAt: number; // epoch milliseconds, not a Date
}
Functions
import {
createConfigEntry, isConfigEntry,
toSafeConfigEntry, serializeConfigEntry,
sortConfigEntries, ConfigSourceType
} from '@zudojs/config';
const entry = createConfigEntry({
key: 'db.password',
value: 'secret123',
source: 'environment',
sourceType: ConfigSourceType.ENVIRONMENT,
priority: 100,
sensitive: true,
});
isConfigEntry(entry); // true
toSafeConfigEntry(entry); // value: '[REDACTED]'
serializeConfigEntry(entry); // plain object (redacted)
CONFIG SOURCE
Sources are the entry point for configuration data. They load key-value pairs from various origins.
Source Types
enum ConfigSourceType {
DEFAULTS = 'defaults', // createDefaultsConfigSource, priority -1000
ENVIRONMENT = 'environment', // createEnvironmentConfigSource (process.env)
FILE = 'file', // type tag only — no built-in file loader; use createCustomConfigSource
MEMORY = 'memory', // createMemoryConfigSource
REMOTE = 'remote', // type tag for custom remote loaders
CUSTOM = 'custom', // createCustomConfigSource
}
Priority
Higher priority wins. Equal priorities are applied in registration order, so the last registered overwrites the earlier one. Only createDefaultsConfigSource (-1000) and createEnvironmentConfigSource (100) pick a priority for you; every other factory falls back to DEFAULT_CONFIG_SOURCE_PRIORITY.
| Layer |
Priority |
| createDefaultsConfigSource | -1000 |
A source that declares no priority | DEFAULT_CONFIG_SOURCE_PRIORITY = -1 |
initialValues and other baseline store writes | 0 |
| createEnvironmentConfigSource | 100 |
Changed in v1.2.0 — an undeclared source now ranks at -1, not 0. DEFAULT_CONFIG_SOURCE_PRIORITY is newly exported and is strictly below the 0 that initialValues is seeded at. Both defaulted to 0 before, and because a source overwrites on equal priority, any source created without one silently wiped a manager's initialValues during load(). Sources that declare priority: 0 or above still override them, as documented. If you relied on an undeclared source beating another source that declares priority: 0, declare a priority on it — that pair has swapped order.
Creating Sources
import { readFile } from 'node:fs/promises';
import {
createMemoryConfigSource,
createDefaultsConfigSource,
createEnvironmentConfigSource,
createCustomConfigSource,
createConfigSource,
ConfigSourceType
} from '@zudojs/config';
// In-memory source — declares no priority, so it lands at -1
// The second argument is required and must carry a name.
const memSource = createMemoryConfigSource({
'app.name': 'My App',
'app.port': 3000,
}, { name: 'memory' });
// Defaults source (lowest priority)
const defaults = createDefaultsConfigSource({
'app.port': 8080,
'app.debug': false,
});
// Custom async source
const remote = createCustomConfigSource('remote', async (ctx) => {
const res = await fetch('https://api.example.com/config');
const data = (await res.json()) as Record<string, string>; // json() is typed unknown
return { values: data, source: 'remote', type: ConfigSourceType.CUSTOM };
});
// Environment variables
const envSource = createEnvironmentConfigSource();
// JSON file — there is no built-in file loader, so write one
const fileSource = createCustomConfigSource('file', async () => ({
values: JSON.parse(await readFile('config.json', 'utf8')),
source: 'file',
type: ConfigSourceType.FILE,
}));
envSource and fileSource are the sources the loader and manager examples below use.
Loading Sources
import { loadConfigSources } from '@zudojs/config';
// Load multiple sources in priority order
const results = await loadConfigSources([defaults, memSource, remote], {
environment: 'production',
namespace: 'myapp',
});
CONFIG STORE
The central in-memory key-value store backed by a Map. Supports change events, snapshots, and prefix-based queries.
Creating a Store
import { createConfigStore } from '@zudojs/config';
const store = createConfigStore({
initialValues: {
'app.name': 'My App',
'app.port': 3000,
},
freeze: true, // Deep freeze all values
});
Reading and Writing
store.has('app.name'); // true
store.get('app.name'); // 'My App'
store.getOrDefault('missing', 42); // 42
store.set('app.debug', true);
store.setMany({ 'a': 1, 'b': 2 });
store.delete('app.debug');
// Get all entries with metadata
const entries = store.getEntries();
const keys = store.keys(); // ['app.name', 'app.port']
const obj = store.toObject(); // { 'app.name': 'My App', ... }
Redaction on Write
Since v1.2.0 set() classifies the entry as it writes it, so a secret is marked sensitive whichever door it came in by — not only when a source supplied it. toObject() always returns RAW values; use toSafeObject() for anything you log or print.
store.set('db.password', 's3cret');
store.set('db.url', 'postgres://user:pw@host/db');
store.set('app.port', 3000);
store.toObject();
// { 'db.password': 's3cret', 'db.url': 'postgres://user:pw@host/db', 'app.port': 3000 }
store.toSafeObject();
// { 'db.password': '[REDACTED]', 'db.url': '[REDACTED]', 'app.port': 3000 }
// Before v1.2.0 both secrets printed in clear here: they were set(), not loaded.
// Opt a key out explicitly.
store.set('api_key_name', 'billing', { sensitive: false });
Prefix Queries
// Get all entries with a given prefix
// 'db' and 'db.' select the same entries (a trailing dot is ignored since v1.3.0;
// before, 'db.' matched nothing and returned [] and {}).
const dbEntries = store.getByPrefix('db');
// Get values as an object with prefix stripped
const dbConfig = store.getObjectByPrefix('db');
// { host: 'localhost', port: 5432 } (not 'db.host', 'db.port')
Change Events
const unsubscribe = store.subscribe((event) => {
console.log(`Changed: ${event.key}`);
// previous and current are whole ConfigEntry objects (or undefined), not values.
console.log(`Previous: ${String(event.previous?.value)}`);
console.log(`Current: ${String(event.current?.value)}`);
});
store.set('app.port', 4000); // Fires listener
unsubscribe(); // Stop listening
Snapshot and Replace
// Create an immutable copy
const snapshot = store.snapshot();
// Replace all values (deletes keys not in new values)
store.replace({
'app.name': 'New App',
'app.port': 8080,
});
CONFIG RESOLVER
Type-safe value resolution from the store with automatic parsing, schema validation, and scoped access.
Creating a Resolver
import { createConfigResolver } from '@zudojs/config';
const resolver = createConfigResolver(store, {
strict: false, // Don't throw on invalid types
allowUndefined: true, // Return undefined for missing keys
clone: true, // Clone values on read
});
Typed Accessors
// String (always returns string or undefined)
resolver.string('app.name'); // 'My App'
resolver.string('missing', 'default'); // 'default'
resolver.requiredString('app.name'); // throws if missing
// Number (auto-parses from string)
resolver.number('app.port'); // 3000
resolver.number('env.port', 8080); // 8080 if missing; typed number
// Boolean (parses "true"/"1"/"yes"/"y"/"on")
resolver.boolean('app.debug'); // true
// Date (parses ISO strings)
resolver.date('app.created'); // Date object
// Object and Array
resolver.object<DbConfig>('db'); // typed object
resolver.array<string>('app.tags'); // typed array
// Raw value with a fallback: the literal is widened, so this is a string
const mode: string = resolver.get('app.mode', 'dev');
Changed in v1.3.0 — a fallback narrows the return type. Called with a fallback, every typed accessor — string, number, boolean, bigint, date, object, array, on the resolver, the manager and a scoped resolver — returns T instead of T | undefined, so const port: number = resolver.number('env.port', 8080) compiles under strict tsc. Calls without a fallback keep T | undefined. The new get(key, fallback) overload returns the stored value, or the fallback when the key is missing; its literal fallback is widened through ConfigWiden<T>, so get('mode', 'dev') is typed string, not 'dev'. Note that get() does no parsing: it returns whatever is stored, so an environment value is still a string there.
Schema Validation
import { ConfigValueType } from '@zudojs/config';
import type { ConfigNumberSchema } from '@zudojs/config';
// resolve() takes a TypedConfigSchema, keyed on `type`: a NUMBER schema accepts
// min/max/integer/positive, and { type: NUMBER, minLength: 1 } is a compile error.
const port = resolver.resolve('app.port', { type: ConfigValueType.NUMBER, min: 1, max: 65535 });
// A named schema still works, typed with the matching interface.
const portSchema: ConfigNumberSchema = {
type: ConfigValueType.NUMBER,
min: 1,
max: 65535,
integer: true,
};
// Full diagnostic result
const result = resolver.resolveResult('app.port', portSchema);
// { key: 'app.port', value: 3000, found: true, valid: true, issues: [] }
Scoped Resolver
// Create a sub-resolver with a key prefix
const db = resolver.scoped('database');
db.string('host'); // reads 'database.host'
db.number('port'); // reads 'database.port'
db.string('name'); // reads 'database.name'
// Nested scoping
const dbPool = db.scoped('pool');
dbPool.number('maxSize'); // reads 'database.pool.maxSize'
Standalone Value Parsers
These take a raw value — typically a string straight from process.env — and return it coerced, or undefined when the input is undefined. They do not read from a store and they do not apply defaults; that is the resolver's job. Use them when you have a value in hand and only need the coercion rules.
import { parseConfigString, parseConfigNumber, parseConfigBoolean } from '@zudojs/config';
const name = parseConfigString(process.env.APP_NAME); // string | undefined
const port = parseConfigNumber(process.env.PORT); // number | undefined
const debug = parseConfigBoolean(process.env.DEBUG); // boolean | undefined
// Supply your own default with ?? — the parsers never invent one.
const resolvedPort = port ?? 3000;
parseConfigBigInt and parseConfigDate follow the same shape.
They never throw. An input that cannot be coerced comes back as undefined, exactly like a missing one — parseConfigNumber("abc") is undefined, not NaN and not an error. That means parseConfigNumber(process.env.PORT) ?? 3000 silently falls back to 3000 when PORT=abc. If a malformed value should stop startup instead, check for undefined yourself, or use ConfigResolver, which reports failures as ConfigResolutionError. parseConfigNumber (and the resolver's number()) accepts decimal notation only; hex, binary and octal are rejected. parseConfigBoolean accepts true/1/yes/y/on and false/0/no/n/off case-insensitively; anything else is undefined.
CONFIG SCHEMA
Schema validation for configuration values with type checking, constraints, defaults, and custom validators.
Schema Types
enum ConfigValueType {
STRING = 'string',
NUMBER = 'number',
BOOLEAN = 'boolean',
BIGINT = 'bigint',
DATE = 'date',
OBJECT = 'object',
ARRAY = 'array',
NULL = 'null',
ANY = 'any',
}
Schema Interfaces
// Base schema
interface ConfigSchema<T extends ConfigValue = ConfigValue> {
type: ConfigValueType | readonly ConfigValueType[]; // every field is readonly
required?: boolean;
nullable?: boolean;
default?: T | (() => T);
description?: string;
secret?: boolean; // honoured at any depth; the store entry holding it is redacted whole
coerce?: boolean; // default true: parse a string for NUMBER/BOOLEAN before the type check
validate?: (value: T, context: ConfigValidationContext) => boolean | string | ConfigValidationIssue | readonly ConfigValidationIssue[];
transform?: (value: ConfigValue, context: ConfigValidationContext) => T;
}
// String schema
interface ConfigStringSchema extends ConfigSchema<string> {
type: ConfigValueType.STRING;
minLength?: number;
maxLength?: number;
pattern?: string | RegExp;
enum?: readonly string[];
}
// Number schema
interface ConfigNumberSchema extends ConfigSchema<number> {
type: ConfigValueType.NUMBER;
min?: number;
max?: number;
integer?: boolean;
positive?: boolean;
}
Since v1.3.0 the constraint groups are exported on their own as ConfigStringConstraints, ConfigNumberConstraints, ConfigArrayConstraints and ConfigObjectConstraints, and ConfigStringSchema / ConfigNumberSchema are built from them with the same fields as before. TypedConfigSchema<T> is the union keyed on type that resolve() and resolveResult() accept, so each value type takes exactly the constraints the validator enforces.
Order of Checks
A value goes through five steps, in this order: coerce → type check → constraints → transform → validate. validate therefore sees the final value, matching its (value: T) signature. Before v1.3.0 validate ran before transform and saw the raw input.
- Coerce. A string is parsed first when the schema's type includes
NUMBER or BOOLEAN and does not also accept STRING or ANY. Numbers are parsed strictly in decimal ("8080" passes; "80a", "0x1F90" and "" stay TYPE_MISMATCH). Booleans use true/false, 1/0, yes/no, y/n, on/off, case-insensitively. Set coerce: false on the schema to require a real number or boolean.
- Transform as a parser. A string that does not have the schema's type is handed to
transform, and the output must then have the type and pass the constraints. A non-string of the wrong type is rejected without calling transform.
import { validateConfigValue, ConfigValueType } from '@zudojs/config';
validateConfigValue('8080', { type: ConfigValueType.NUMBER }).value; // 8080
validateConfigValue('yes', { type: ConfigValueType.BOOLEAN }).value; // true
validateConfigValue('80a', { type: ConfigValueType.NUMBER }).valid; // false (TYPE_MISMATCH)
validateConfigValue('8080', { type: ConfigValueType.NUMBER, coerce: false }).valid; // false
// transform parses a string that does not have the type; validate sees the result
validateConfigValue('a,b', {
type: ConfigValueType.ARRAY,
transform: (s) => String(s).split(','),
}).value; // ['a', 'b']
validateConfigValue('1F90', {
type: ConfigValueType.NUMBER,
transform: (s) => parseInt(String(s), 16),
validate: (port) => port > 1024,
}).value; // 8080
Validation
import { validateConfigObject, assertValidConfig, ConfigValueType } from '@zudojs/config';
import type { ConfigObjectSchema } from '@zudojs/config';
const schema: ConfigObjectSchema = {
type: ConfigValueType.OBJECT,
properties: {
'app.name': { type: ConfigValueType.STRING, required: true, minLength: 1 },
'app.port': { type: ConfigValueType.NUMBER, min: 1, max: 65535, integer: true },
'app.debug': { type: ConfigValueType.BOOLEAN, default: false },
}
};
const result = validateConfigObject(configValues, schema);
// { valid: true, value: {...}, issues: [] }
// Or throw on validation failure
assertValidConfig(configValues, schema);
// Throws ConfigSchemaValidationError if invalid
Validation Error
import { assertValidConfig, ConfigSchemaValidationError } from '@zudojs/config';
try {
assertValidConfig(values, schema);
} catch (error) {
if (error instanceof ConfigSchemaValidationError) {
console.log(error.issues);
// [{ path: '$.app.port', message: 'Value must be greater than or equal to 1.',
// code: 'MIN', severity: 'error' }]
}
}
The manager's own error carries the same issue objects. Since v1.3.1 ConfigManagerValidationError.issues is typed readonly ConfigValidationIssue[], so you read path, code and message without a cast (it was readonly unknown[] before):
import { configFactory, ConfigManagerValidationError, ConfigValueType } from '@zudojs/config';
try {
await configFactory.validated(
{
properties: {
'app.name': { type: ConfigValueType.STRING, required: true },
'app.port': { type: ConfigValueType.NUMBER, min: 1, max: 65535 },
},
},
{ initialValues: { 'app.port': 0 } },
);
} catch (error) {
if (error instanceof ConfigManagerValidationError) {
for (const issue of error.issues) console.log(issue.path, issue.code, issue.message);
}
}
// $.app.name REQUIRED Configuration value at "$.app.name" is required.
// $.app.port MIN Value must be greater than or equal to 1.
CONFIG LOADER
Orchestrates loading configuration from multiple sources into the store with priority-based resolution.
Creating a Loader
import { createConfigLoader } from '@zudojs/config';
const loader = createConfigLoader({
sources: [defaultsSource, envSource, fileSource],
context: { environment: 'production' },
freeze: true,
onSourceLoaded: (source, result) => {
console.log(`Loaded ${Object.keys(result.values).length} values from ${source.name}`);
},
onSourceError: (source, error) => {
// error is typed unknown
const message = error instanceof Error ? error.message : String(error);
console.error(`Failed to load ${source.name}: ${message}`);
},
});
Loading
// Load all sources
const result = await loader.load();
// { store, entries, sources, loadedAt }
// Reload from scratch
await loader.reload();
// Load specific sources only
await loader.loadSources([envSource]);
Reload rebuilds source values atomically; values a source stopped providing are removed.
Managing Sources
loader.addSource(newSource);
loader.removeSource('remote');
const src = loader.getSource('environment');
Sources are deduplicated by name, first occurrence wins. Since v1.2.0 that also applies to the sources array passed to createConfigLoader — the same rule addSource() and loadConfigSources() already enforced. Before v1.2.0 a duplicate name in the constructor array was loaded twice, with the last one winning, the opposite of every other path.
One-Shot Loading
import { loadConfiguration } from '@zudojs/config';
// Create, load, and dispose in one call
const result = await loadConfiguration(sources, {
context: { environment: 'production' },
});
CONFIG MANAGER
The central orchestration class that coordinates the loader, store, and resolver with a lifecycle state machine.
Lifecycle States
enum ConfigManagerState {
CREATED = 'created', // Initial state
LOADING = 'loading', // Loading from sources
READY = 'ready', // Configuration available
RELOADING = 'reloading', // Reloading from sources
FAILED = 'failed', // Load failed
DISPOSED = 'disposed', // Manager disposed
}
Creating a Manager
import { createConfigManager, initializeConfigManager } from '@zudojs/config';
// Create without loading
const manager = createConfigManager({
sources: [envSource, fileSource],
initialValues: { 'app.name': 'My App' },
freeze: true,
strict: false,
});
// Create and load in one step
const loadedManager = await initializeConfigManager({
sources: [envSource],
initialValues: { 'app.port': 3000 },
});
Lifecycle Methods
await manager.load(); // CREATED → LOADING → READY
await manager.reload(); // READY → RELOADING → READY
manager.getState(); // 'ready' (ConfigManagerState.READY)
manager.isReady; // true
manager.isLoading; // false
Typed Access (Same as Resolver)
manager.string('app.name');
manager.number('app.port');
manager.boolean('app.debug');
manager.required<string>('app.name');
manager.object<DbConfig>('db');
manager.array<string>('app.tags');
manager.scoped('db');
// Typed required accessors (on the manager since v1.3.0): parse, check, throw.
manager.requiredNumber('app.port');
manager.requiredString('app.name');
manager.requiredBoolean('app.debug');
manager.requiredDate('app.created');
required<T>() is a cast, not a parse. It returns the stored value unchanged and only tells the compiler it is a T, because T is erased at run time and cannot be checked. With DB__PORT=5432 from the environment, manager.required<number>('db.port') returns the string "5432" typed as number. Use requiredNumber() / requiredBoolean() / requiredString() / requiredDate() instead — on the manager, the resolver or a scoped resolver — which parse the value and throw ConfigResolutionError when it is missing or does not parse.
Runtime Updates
manager.set('app.debug', true);
manager.delete('app.debug');
manager.toObject(); // all values as plain object — RAW, secrets included
manager.toSafeObject(); // sensitive values replaced with '[REDACTED]'
// Since v1.2.0 a runtime set() is screened like any other write,
// so this is redacted by toSafeObject() — it was not before.
manager.set('db.password', 's3cret');
Log toSafeObject(), never toObject(). Values seeded through initialValues are seeded with source: "initialValues" on every path (a manager-created store recorded "runtime" before v1.2.0), and they survive load() and reload().
Source Management
manager.addSource(newSource);
manager.addSourceLoader('flags', async () => ({
values: { 'feature.beta': true },
source: 'flags',
type: ConfigSourceType.CUSTOM,
}));
manager.removeSource('old-source');
Validation and Status
// Validate entire config against a schema
const config = manager.validate<AppConfig>(appSchema);
// Get status snapshot
const status = manager.getStatus();
// { name: 'config', state: 'ready', loaded: true, loading: false, size: 42,
// lastLoadedAt: 1790176359562 } — epoch milliseconds, not a Date
// Subscribe to lifecycle STATE changes (load, reload, failure, dispose).
// It does not fire on set()/delete(); use manager.getStore().subscribe() for values.
const unsub = manager.subscribe((status) => {
console.log(`State: ${status.state}`);
});
Validating Environment Values
createEnvironmentConfigSource() stores every variable as the raw string it read: APP__PORT=8080 becomes 'app.port': '8080' (__ maps to ., and names are lower-cased). Since v1.3.0 a NUMBER or BOOLEAN schema coerces such strings before the type check (see Order of Checks), so validate(), configFactory.validated(), resolve() and validateConfigObject() accept them and return the parsed values. The store itself keeps the strings; read through number() / boolean() or the value validate() returns. (In v1.2.0 these schemas always failed on environment values with TYPE_MISMATCH.)
import {
createConfigManager, createEnvironmentConfigSource, ConfigValueType,
} from '@zudojs/config';
// APP__PORT=8080 APP__DEBUG=yes
const manager = createConfigManager({
sources: [createEnvironmentConfigSource({ prefix: 'APP__' })],
});
await manager.load();
const config = manager.validate<{ port: number; debug: boolean }>({
properties: {
port: { type: ConfigValueType.NUMBER, required: true, integer: true, min: 1, max: 65535 },
debug: { type: ConfigValueType.BOOLEAN, default: false },
},
});
console.log(config.port, config.debug); // 8080 true (a number and a boolean)
// Or read one value with a parsing accessor
const port = manager.requiredNumber('port'); // 8080
A value that does not parse (APP__PORT=80a) still fails with TYPE_MISMATCH and validate() throws ConfigManagerValidationError. For richer parsing across the whole object you can still coerce with @zudojs/schema.
Cleanup
await manager.dispose();
// Closes all sources, clears store, removes listeners
CONFIG FACTORY
High-level convenience functions for common configuration patterns.
import { configFactory } from '@zudojs/config';
// Create without loading
const created = configFactory.create({ initialValues });
// Create and load
const loaded = await configFactory.initialize({ sources });
// Create from key-value map
const fromValues = configFactory.fromValues({ 'key': 'value' });
// Create from sources
const fromSources = configFactory.fromSources([source1, source2]);
// Create from existing store
const fromStore = configFactory.fromStore(existingStore);
// Create, load, and validate
// validated(schema, options) — the schema is the first argument.
const { manager, config } = await configFactory.validated(appSchema, { sources });
TYPES REFERENCE
All exported types, interfaces, and type aliases.
| Type |
Module |
Description |
| ConfigPrimitive | configValue | string | number | boolean | bigint | null | undefined |
| ConfigJsonValue | configValue | JSON-compatible recursive type |
| ConfigValue | configValue | Full config value type |
| ConfigEntry<T> | configEntry | Resolved entry with metadata |
| ConfigEntryOptions<T> | configEntry | Options for creating an entry |
| ConfigSource | configSource | Full source contract |
| ConfigSourceEntry | configSource | Single value from a source |
| ConfigSourceContext | configSource | Load context |
| ConfigSourceResult | configSource | Source load result |
| ConfigChangeEvent | configStore | Change event payload |
| ConfigStoreOptions | configStore | Store initialization options |
| ConfigResolverOptions | configResolver | Resolver options |
| ConfigResolutionResult<T> | configResolver | Full diagnostic result |
| ConfigSchema<T> | configSchema | Base schema interface |
| ConfigObjectSchema<T> | configSchema | Object schema |
| ConfigArraySchema<T> | configSchema | Array schema |
| ConfigStringSchema | configSchema | String schema with constraints |
| ConfigNumberSchema | configSchema | Number schema with constraints |
| TypedConfigSchema<T> | configSchema | Union keyed on type accepted by resolve() / resolveResult() (v1.3.0) |
| ConfigStringConstraints, ConfigNumberConstraints, ConfigArrayConstraints, ConfigObjectConstraints | configSchema | The constraint fields each value type accepts (v1.3.0) |
| ConfigWiden<T> | configResolver | Widens a literal fallback to its primitive type in get(key, fallback) (v1.3.0) |
| ConfigValidationIssue | configSchema | Validation issue |
| ConfigValidationResult | configSchema | Validation result |
| ConfigManagerOptions | configManager | Manager options |
| ConfigManagerStatus | configManager | Manager status snapshot |
| ConfigLoaderOptions | configLoader | Loader options |
| ConfigLoadResult | configLoader | Load result |
| ConfigFactoryOptions | configFactory | Factory options |
CONSTANTS
All exported enums and constants, and their values.
DEFAULT_CONFIG_SOURCE_PRIORITY
DEFAULT_CONFIG_SOURCE_PRIORITY = -1 // priority given to a source that declares none
New in v1.2.0. Strictly below the 0 that initialValues and other baseline store writes use, so an undeclared source layers under them instead of overwriting them.
ConfigSourceType
DEFAULTS = 'defaults' // createDefaultsConfigSource, priority -1000
ENVIRONMENT = 'environment' // createEnvironmentConfigSource (process.env)
FILE = 'file' // type tag only — no built-in file loader; use createCustomConfigSource
MEMORY = 'memory' // createMemoryConfigSource
REMOTE = 'remote' // type tag for custom remote loaders
CUSTOM = 'custom' // createCustomConfigSource
ConfigValueType
STRING = 'string' | NUMBER = 'number' | BOOLEAN = 'boolean'
BIGINT = 'bigint' | DATE = 'date' | OBJECT = 'object'
ARRAY = 'array' | NULL = 'null' | ANY = 'any'
ConfigValidationSeverity
ERROR = 'error'
WARNING = 'warning'
ConfigManagerState
CREATED = 'created' // Initial state
LOADING = 'loading' // Loading from sources
READY = 'ready' // Configuration available
RELOADING = 'reloading' // Reloading from sources
FAILED = 'failed' // Load failed
DISPOSED = 'disposed' // Manager disposed
ERRORS
All error classes, their fields, and when they are thrown.
| Class |
Extends |
Fields |
Thrown When |
| ConfigManagerValidationError |
ConfigurationError |
issues: readonly ConfigValidationIssue[] (typed since v1.3.1; was readonly unknown[]) |
manager.validate() or configFactory.validated() fails against the schema |
| ConfigSchemaValidationError |
ConfigurationError |
issues: ConfigValidationIssue[] |
assertValidConfig() fails |
| ConfigResolutionError |
ConfigurationError |
key: string, issues: unknown[] |
Type mismatch in strict mode, missing required value |
Re-exported from @zudojs/errors
import {
ConfigurationError,
createConfigurationError,
isConfigurationError,
missingConfigurationError,
invalidConfigurationError,
} from '@zudojs/config';
USE CASES
1. Application Configuration with Environment Variables
const manager = await initializeConfigManager({
sources: [
createDefaultsConfigSource({ 'app.port': 3000 }), // priority -1000
createEnvironmentConfigSource(), // priority 100
],
});
// APP__PORT=8080 is read as 'app.port': '8080' (a string) and overrides the default.
const port = manager.number('app.port') ?? 3000; // 8080 — number() parses the string
2. Multi-Source Configuration with Priority
const manager = await initializeConfigManager({
sources: [
createDefaultsConfigSource(defaults), // priority: -1000
fileSource, // priority: -1 (declares none)
createCustomConfigSource('env', envLoader, { priority: 100 }),
],
});
// Higher priority wins when keys overlap
3. Type-Safe Configuration Access
// The required* helpers are on the manager, the resolver and scoped resolvers.
const db = manager.scoped('db');
const host = db.requiredString('host'); // string; throws if missing
const port = db.requiredNumber('port'); // number; parses "5432", throws if it cannot
// A fallback makes the result non-optional.
const poolSize: number = db.number('poolSize', 10);
4. Configuration Validation with Schemas
const { manager, config } = await configFactory.validated(
{
properties: {
'app.port': { type: ConfigValueType.NUMBER, min: 1, max: 65535 },
'app.name': { type: ConfigValueType.STRING, required: true },
},
},
{ initialValues: { 'app.name': 'My App', 'app.port': 3000 } },
);
// Throws ConfigManagerValidationError if validation fails.
// NUMBER/BOOLEAN also accept environment strings such as "8080" (coerced).
5. Runtime Configuration Updates
// Update a value at runtime
manager.set('app.debug', true);
// Listen for value changes on the store (manager.subscribe() only
// reports lifecycle state changes, not set() or delete()).
manager.getStore().subscribe((event) => {
console.log(`Config updated: ${event.key}`);
});
// Reload from sources
await manager.reload();
6. Scoped Configuration for Modules
// Each module gets its own scoped resolver
const authConfig = manager.scoped('auth');
const dbConfig = manager.scoped('db');
const cacheConfig = manager.scoped('cache');
// Modules access only their own config
const secret = authConfig.string('jwtSecret');
if (secret === undefined) throw missingConfigurationError('auth.jwtSecret');
const host = dbConfig.string('host') ?? 'localhost';
const ttl = cacheConfig.number('ttl') ?? 3600;
CONNECTIONS
How @zudojs/config integrates with other Zudo packages.
IMPROVEMENTS
Best Practices
- Use scoped resolvers — Isolate module configuration with
manager.scoped('module') to prevent accidental cross-module access.
- Validate early — Use
configFactory.validated() at startup to catch configuration errors before the app runs.
- Log
toSafeObject() — Secrets are detected and marked on every write since v1.2.0, whatever path they arrived by, but only toSafeObject() honours that mark. toObject() returns them raw. Pass sensitive: true to mark a key the detector would miss, or sensitive: false to opt one out.
- Use priority ordering — Set environment sources at higher priority than file sources, and file sources higher than defaults.
- Freeze in production — Enable
freeze: true to prevent accidental mutation of configuration values.
- Subscribe to changes — Use
store.subscribe() to react to runtime configuration updates.
- Use typed accessors — Prefer
manager.string(), manager.number() over raw manager.get() for type safety.
- Handle reload gracefully — Subscribe to manager state changes to respond to configuration reloads.
API SUMMARY
Compact reference of all exports organized by submodule.
configValue
ConfigPrimitive, ConfigJsonValue, ConfigValue, ResolvedConfigValue, isConfigPrimitive, isConfigObject, isConfigValue, toConfigJsonValue, configValueToString, parseConfigString, parseConfigBoolean, parseConfigNumber, parseConfigBigInt, parseConfigDate, freezeConfigValue, cloneConfigValue, configValuesEqual
configEntry
ConfigEntry, ConfigEntryOptions, createConfigEntry, updateConfigEntry, isConfigEntry, redactConfigValue, toSafeConfigEntry, serializeConfigEntry, withConfigEntrySource, configEntriesEqual, sortConfigEntries
configSource
ConfigSourceType, ConfigSourceEntry, ConfigSourceContext, ConfigSourceResult, ConfigSource, ConfigSourceLoader, FunctionConfigSource, ConfigSourceOptions, DEFAULT_CONFIG_SOURCE_PRIORITY, isConfigSource, createConfigSource, createMemoryConfigSource, createDefaultsConfigSource, createCustomConfigSource, createEnvironmentConfigSource, isSensitiveConfigKey, isSensitiveConfigValue, isSensitiveConfigEntry, normalizeConfigSourceResult, sortConfigSources, findConfigSource, deduplicateConfigSources, loadConfigSource, loadConfigSources
configStore
ConfigStore, ConfigChangeEvent, ConfigChangeListener, ConfigStoreOptions, createConfigStore, normalizeKey
configResolver
ConfigResolver, ScopedConfigResolver, ConfigResolverOptions, ConfigResolutionResult, ConfigResolutionError, ConfigWiden, createConfigResolver
configSchema
ConfigValueType, ConfigValidationSeverity, ConfigValidationIssue, ConfigValidationResult, ConfigValidationContext, ConfigSchema, ConfigObjectSchema, ConfigArraySchema, ConfigStringSchema, ConfigNumberSchema, ConfigBooleanSchema, TypedConfigSchema, ConfigStringConstraints, ConfigNumberConstraints, ConfigArrayConstraints, ConfigObjectConstraints, ConfigSchemaDefinition, ConfigSchemaBuilder, getConfigValueType, matchesConfigType, createConfigValidationIssue, validateConfigValue, validateConfigObject, assertValidConfig, ConfigSchemaValidationError
configLoader
ConfigLoader, ConfigLoaderOptions, ConfigLoadResult, createConfigLoader, loadConfiguration, sourceResultsToEntries
configManager
ConfigManager, ConfigManagerState, ConfigManagerOptions, ConfigManagerStatus, ConfigManagerListener, ConfigManagerValidationError, createConfigManager, initializeConfigManager
configFactory
configFactory, createConfiguration, createInitializedConfiguration, createConfigurationFromValues, createConfigurationFromSources, createConfigurationFromStore, createValidatedConfiguration, ConfigFactoryOptions
Re-exports from @zudojs/errors
ConfigurationError, createConfigurationError, isConfigurationError, missingConfigurationError, invalidConfigurationError
COMPLETE EXPORT INDEX
Every name @zudojs/config exports from its package root at v1.2.0 — 114 in total, generated from the package’s own entry point rather than written by hand. The sections above explain the ones you reach for most; this is the exhaustive list, so nothing shipped is undocumented. Names not covered above are typically internal helpers and supporting types.
Show all 114 exports
Classes (9)
ConfigLoader ConfigManager ConfigManagerValidationError ConfigResolutionError ConfigResolver ConfigSchemaValidationError ConfigStore ConfigurationError ScopedConfigResolver
Functions (65)
assertValidConfig cloneConfigValue configEntriesEqual configValuesEqual configValueToString createConfigEntry createConfigLoader createConfigManager createConfigResolver createConfigSource createConfigStore createConfiguration createConfigurationError createConfigurationFromSources createConfigurationFromStore createConfigurationFromValues createConfigValidationIssue createCustomConfigSource createDefaultsConfigSource createEnvironmentConfigSource createInitializedConfiguration createMemoryConfigSource createValidatedConfiguration deduplicateConfigSources defineConfigProperty findConfigSource freezeConfigValue getConfigValueType initializeConfigManager invalidConfigurationError isConfigEntry isConfigObject isConfigPrimitive isConfigSource isConfigurationError isConfigValue isSensitiveConfigEntry isSensitiveConfigKey isSensitiveConfigValue isUnsafeConfigKey loadConfigSource loadConfigSources loadConfigSourceStrict loadConfiguration matchesConfigType missingConfigurationError normalizeConfigSourceResult normalizeKey parseConfigBigInt parseConfigBoolean parseConfigDate parseConfigNumber parseConfigString readOwnConfigProperty redactConfigValue serializeConfigEntry sortConfigEntries sortConfigSources sourceResultsToEntries toConfigJsonValue toSafeConfigEntry updateConfigEntry validateConfigObject validateConfigValue withConfigEntrySource
Interfaces (25)
ConfigArraySchema ConfigBooleanSchema ConfigChangeEvent ConfigEntry ConfigEntryOptions ConfigLoaderOptions ConfigLoadResult ConfigManagerOptions ConfigManagerStatus ConfigNumberSchema ConfigObjectSchema ConfigResolutionResult ConfigResolverOptions ConfigSchema ConfigSource ConfigSourceContext ConfigSourceOptions ConfigSourceResult ConfigStoreOptions ConfigStringSchema ConfigValidationContext ConfigValidationIssue ConfigValidationResult EnvironmentConfigSourceOptions FunctionConfigSource
Type aliases (9)
AnyConfigSchema ConfigChangeListener ConfigFactoryOptions ConfigJsonValue ConfigManagerListener ConfigPrimitive ConfigSourceLoader ConfigValue ResolvedConfigValue
Constants (2)
configFactory DEFAULT_CONFIG_SOURCE_PRIORITY
Enums (4)
ConfigManagerState ConfigSourceType ConfigValidationSeverity ConfigValueType