Configuration
Read settings once at boot. Parse environment values, validate the bits your application needs and pass them into the services that use them.
On this page
Source-backed MarkdownConfigure a notes application
Complete Installation, then run this small configuration lab from your independent app directory. It needs no SQL, HTTP server or API key. The example reads a page size and local port, controls debug mode and requires a webhook key only when that optional integration is enabled.
There is no automatic config-folder discovery. Import your own config files explicitly. The runner loads .env through dotenv/config before reading values; the Config service itself does not load environment files. Keep .env out of Git.
mkdir -p examples
cp -R node_modules/@db3.ai/app/src/config/examples/. examples/Run it
With no application overrides, expect name: "My notes", port: 3000, pageSize: 20, debug: false and notesUrl: "http://localhost:3000/notes". It builds the URL but does not start a listener.
Run the override command to change the page size and port. Setting APP_PORT=abc produces an error naming APP_PORT before App construction. Correct it and run again. Existing shell variables take precedence over values loaded by dotenv.
npx tsx examples/runConfig.ts
APP_PORT=3100 NOTES_PAGE_SIZE=50 APP_DEBUG=yes npx tsx examples/runConfig.ts
# This should fail; correct the value and repeat.
APP_PORT=abc npx tsx examples/runConfig.tsParse and validate at the boundary
createEnv(source) makes an isolated reader. Tests can pass an ordinary object without changing global environment state. defineConfig() preserves inferred types; it does not validate values, merge files or make them immutable.
env.integer() rejects fractions, but a valid integer can still be an invalid port or page size. Keep those application rules beside the configuration. Do not call env.required() for an optional feature until that feature is enabled.
import { createEnv, defineConfig, type EnvSource } from '@db3.ai/app/config';
/**
* Reads application settings once at boot, before constructing services.
*
* Parsing a number does not establish its business range. Optional integration
* credentials are required only when that integration is explicitly enabled.
*
* @param source - Environment values; inject a plain object in application tests.
* @returns Validated, typed settings owned by this notes application.
*/
export function loadNotesConfig(source: EnvSource = process.env) {
const env = createEnv(source);
const port = env.integer('APP_PORT', 3000);
const pageSize = env.integer('NOTES_PAGE_SIZE', 20);
if (port < 1 || port > 65_535) throw new Error('APP_PORT must be between 1 and 65535.');
if (pageSize < 1 || pageSize > 100) throw new Error('NOTES_PAGE_SIZE must be between 1 and 100.');
const webhookEnabled = env.boolean('WEBHOOK_ENABLED', false);
const webhookKey = webhookEnabled ? env.required('WEBHOOK_KEY') : undefined;
if (webhookEnabled && !webhookKey?.trim()) throw new Error('WEBHOOK_KEY cannot contain only whitespace.');
return defineConfig({
name: env('APP_NAME', 'My notes'),
port,
pageSize,
debug: env.boolean('APP_DEBUG', false),
allowedOrigins: env.array('ALLOWED_ORIGINS', []),
webhook: { enabled: webhookEnabled, key: webhookKey },
});
}
Pass settings into App
new App({ config: { notes } }) exposes the repository through application.config. The example passes URL settings separately through url; a value stored under config.url would not configure that service.
Read typed application settings directly from the returned object when practical. config.get<T>() is useful for service boundaries, but its type argument is a TypeScript assertion, not runtime validation or checked dot-path autocomplete.
import 'dotenv/config';
import { pathToFileURL } from 'node:url';
import type { EnvSource } from '@db3.ai/app/config';
import { App } from '@db3.ai/app/server';
import { loadNotesConfig } from './loadNotesConfig';
/**
* Boots configuration and URL generation without opening HTTP or SQL connections.
*
* @param source - App settings; no process-global environment changes are needed.
* @returns A deliberately selected public summary, never the complete config.
*/
export async function runConfig(source: EnvSource = process.env) {
const notes = loadNotesConfig(source);
const application = new App({
config: { notes },
url: { baseUrl: `http://localhost:${notes.port}` },
dbOptions: { syncColumns: false },
});
try {
return {
name: application.config.get<string>('notes.name'),
port: notes.port,
notesUrl: application.url.to('/notes'),
pageSize: notes.pageSize,
debug: notes.debug,
allowedOrigins: notes.allowedOrigins,
missingValue: application.config.get('notes.missing', 'fallback'),
hasWebhook: application.config.has('notes.webhook'),
webhookEnabled: notes.webhook.enabled,
configReused: application.config === application.config,
};
} finally {
await application.close();
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
console.log(JSON.stringify(await runConfig(), null, 2));
}
Defaults and missing paths
get(path, fallback) uses the fallback only when the property is absent. Existing null, undefined, false, zero and empty strings are preserved. has() tests property presence, not truthiness.
Dot paths traverse own object properties, not array indexes. Read the array as a value. Empty paths or paths containing an empty segment throw. all() returns the original configuration object, not a clone; treat it as read-only and never return it from an API.
Environment parser details
env() and env.string() return raw strings: an empty string is still a value. env.required() rejects missing or exactly empty strings, but does not trim whitespace. The example adds the whitespace check for its key.
Boolean, number, integer, array and JSON parsers trim input and use their fallback for missing or empty values. Boolean accepts 1, true, yes, on, 0, false, no and off, case-insensitively. Invalid booleans, non-finite numbers, fractions passed to integer() and malformed JSON throw.
env.array() splits comma-separated values, trims entries and removes empty entries. env.json<T>() parses JSON but does not validate its shape against T; use Validation or an explicit shape check before trusting it. Readers see later changes to their source object, while your constructed settings are a boot-time snapshot.
Secrets, changes and cleanup
Only expose a deliberate public subset of settings. Do not log config.all(), serialize it into a browser page or prefix a server key with VITE_. The lab output intentionally omits its webhook key.
Restart the process after changing boot-time configuration. Mutating an object or environment variable will not recreate already initialized services. The Config repository owns no handles; the lab still closes its App in finally.
Testing
Create a tests/config directory and save the test below as runConfig.test.ts. It imports the example you copied into examples/. Keep the same folder layout so the relative import resolves.
Run from the application root with the development dependencies from Installation. These are consumer tests, not commands that assume a framework checkout.
import { expect, it } from 'vitest';
import { Config, createEnv } from '@db3.ai/app/config';
import { loadNotesConfig } from '../../examples/loadNotesConfig';
import { runConfig } from '../../examples/runConfig';
it('boots explicit configuration without SQL or network access and returns only selected values', async () => {
expect(await runConfig({})).toEqual({ name: 'My notes', port: 3000, notesUrl: 'http://localhost:3000/notes', pageSize: 20, debug: false, allowedOrigins: [], missingValue: 'fallback', hasWebhook: true, webhookEnabled: false, configReused: true });
const result = await runConfig({ APP_NAME: 'Agency notes', APP_PORT: '3100', APP_DEBUG: 'yes', NOTES_PAGE_SIZE: '50', ALLOWED_ORIGINS: 'https://one.example, https://two.example,', WEBHOOK_ENABLED: 'on', WEBHOOK_KEY: 'test-only-secret' });
expect(result).toMatchObject({ name: 'Agency notes', port: 3100, pageSize: 50, debug: true, allowedOrigins: ['https://one.example', 'https://two.example'], webhookEnabled: true });
expect(JSON.stringify(result)).not.toContain('test-only-secret');
});
it('fails before boot for invalid types, ranges or enabled integrations without credentials', async () => {
for (const source of [{ APP_PORT: 'abc' }, { APP_PORT: '1.5' }, { APP_PORT: '0' }, { NOTES_PAGE_SIZE: '101' }, { APP_DEBUG: 'maybe' }, { WEBHOOK_ENABLED: 'true' }, { WEBHOOK_ENABLED: 'true', WEBHOOK_KEY: ' ' }]) {
expect(() => loadNotesConfig(source)).toThrow();
}
expect(loadNotesConfig({ APP_PORT: '', WEBHOOK_ENABLED: 'false' }).port).toBe(3000);
expect(() => loadNotesConfig({ WEBHOOK_ENABLED: 'true' })).toThrow('WEBHOOK_KEY');
});
it('distinguishes presence from fallback and documents raw strings versus parsed values', () => {
const config = new Config({ notes: { title: '', value: null, optional: undefined, enabled: false, tags: ['one'] } });
expect(config.get('notes.title', 'fallback')).toBe('');
expect(config.get('notes.value', 'fallback')).toBeNull();
expect(config.get('notes.optional', 'fallback')).toBeUndefined();
expect(config.has('notes.optional')).toBe(true);
expect(config.get('notes.enabled', true)).toBe(false);
expect(config.get('notes.tags.0', 'missing')).toBe('missing');
expect(() => config.get('notes..title')).toThrow('empty segment');
const env = createEnv({ EMPTY: '', JSON: '{"enabled":true}', RATIO: '1.5' });
expect(env('EMPTY', 'fallback')).toBe('');
expect(env.integer('EMPTY', 20)).toBe(20);
expect(env.number('RATIO')).toBe(1.5);
expect(env.json('JSON')).toEqual({ enabled: true });
});
Run and extend the tests
The three copied tests cover defaults, overrides, redaction, malformed input, range limits, optional credentials, raw/parsed values and property presence. Change the maximum page size, update its boundary assertion and rerun.
npx vitest run tests/config/runConfig.test.ts
npx tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --types node --skipLibCheck examples/*.tsCoverage and next steps
Taught and tested: boot-time settings, integer/boolean/array parsing, required values, application ranges, Config reuse, fallback/presence, JSON/number parsing and secret-safe output. The service suite covers the remaining parser failures.
Reference-only details are the generic overloads and every individual malformed input. There is no config-file discovery, automatic hot reload, schema validation or secret manager. Those are not hidden framework features.
Boots typed application settings, rejects malformed configuration, preserves dot-path semantics and keeps credentials out of the displayed result.
The guide test passes against the real framework components.packages/app/src/config/tests/examples/runConfig.test.tsThis test command requires the framework repository. Use the walkthrough commands in an installed application.
Environment: Node.js 24; no database, listening port, provider or credentials.