# Configuration

> Read settings once at boot. Parse environment values, validate the bits your application needs and pass them into the services that use them.

- Package: `@db3.ai/app/config`
- Canonical page: [https://db3.ai/docs/config](https://db3.ai/docs/config)
- Markdown: [https://db3.ai/docs/config.md](https://db3.ai/docs/config.md)
- Framework source of truth: `packages/app/src/config/README.md`

<a id="setup"></a>

## Configure 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.

- [Install the preview and development tools](https://db3.ai/docs/installation.md)

### Copy the shipped example

```bash
mkdir -p examples
cp -R node_modules/@db3.ai/app/src/config/examples/. examples/
```

<a id="run"></a>

## 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.

### Defaults, an override and a deliberate failure

```bash
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.ts
```

<a id="load"></a>

## Parse 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.

### examples/loadNotesConfig.ts

```typescript
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 },
	});
}
```

<a id="app"></a>

## 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.

- [App configuration and process ownership](https://db3.ai/docs/app-config.md)

### examples/runConfig.ts

```typescript
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));
}
```

<a id="read"></a>

## 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.

<a id="environment"></a>

## 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.

- [All parser overloads](https://db3.ai/docs/config-api.md#environment)
- [Validate application input](https://db3.ai/docs/validation.md)

<a id="safety"></a>

## 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`.

<a id="testing"></a>

## 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.

### tests/config/runConfig.test.ts

```typescript
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 });
});
```

<a id="run-tests"></a>

## 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.

### Run your copied test and check types

```bash
npx vitest run tests/config/runConfig.test.ts
npx tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --types node --skipLibCheck examples/*.ts
```

<a id="coverage"></a>

## Coverage 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.

- [Config API reference](https://db3.ai/docs/config-api.md)
- [Build the first HTTP app](https://db3.ai/docs/create-app.md)

## Additional source-backed examples

### Run the lab

```bash
npx tsx examples/runConfig.ts
```

## Behavioural verification
Boots typed application settings, rejects malformed configuration, preserves dot-path semantics and keeps credentials out of the displayed result.
- Behaviour test: `packages/app/src/config/tests/examples/runConfig.test.ts`
- Repository test command (framework checkout only): `npm run test:service --workspace @platform/app -- config --maxWorkers=1`
- In an installed application, use the walkthrough commands instead of this repository test.
- Expected outcome: The guide test passes against the real framework components.
- Environment: Node.js 24; no database, listening port, provider or credentials.

## Related documentation
- [Config API reference](https://db3.ai/docs/config-api.md): Current emitted signatures and options for @db3.ai/app/config.
- [Configure the application runtime](https://db3.ai/docs/app-config.md): Keep application settings explicit. Boot one App, give each service its options and leave HTTP and worker startup to the host.
- [App](https://db3.ai/docs/app.md): Create one application at boot. Configure its services, use request-local state and close the resources you own.
- [Validate application input](https://db3.ai/docs/validation.md): Check a note request, return useful field errors and make the boundary between validation, conversion and authorization explicit.

## Framework-owned source: `packages/app/src/config/README.md`

This is the exact source document captured by the documentation build. Use it for detailed API and workflow guidance, subject to the public package exports and behavioural evidence identified above.

````markdown
# Configuration

Read settings once at boot. Parse environment values, validate what your
application needs and pass the result into the services that use it.
`@db3.ai/app/config` supplies `Config`, `defineConfig()`, `env` and `createEnv()`.
It does not discover configuration files or load `.env` for you.

## Run the notes configuration example

Complete the [installation guide](https://db3.ai/docs/installation), including
`tsx`, TypeScript and Vitest. From that independent app directory:

```sh
mkdir -p examples
cp -R node_modules/@db3.ai/app/src/config/examples/. examples/
npx tsx examples/runConfig.ts
APP_PORT=3100 NOTES_PAGE_SIZE=50 APP_DEBUG=yes npx tsx examples/runConfig.ts
```

The default run prints `My notes`, port `3000`, page size `20` and debug `false`.
The override uses port `3100`, page size `50` and debug `true`. A generated URL is
only a value: this lab does not start HTTP, open SQL or contact a provider.
The runner loads `.env` through `dotenv/config`; existing environment variables
take precedence. Keep `.env` out of Git.

Now try an invalid setting:

```sh
APP_PORT=abc npx tsx examples/runConfig.ts
```

It fails with an error naming `APP_PORT` before constructing App. Correct the
value and repeat the command. No partial service startup needs cleaning up.

## Own the application settings

The shipped [loadNotesConfig.ts](./examples/loadNotesConfig.ts) uses
`createEnv(source)` so tests can provide an ordinary object. It validates a
port and page size after parsing them. An integer parser does not know whether
`0` is a valid application port or `100000` is a sensible page size.

Only call `env.required()` when the owning feature needs the value. The example
requires `WEBHOOK_KEY` only when `WEBHOOK_ENABLED` is true. It does not send a
webhook, and it never includes the key in its printed result.

```ts
import { createEnv, defineConfig } from '@db3.ai/app/config';

const env = createEnv({ APP_DEBUG: 'yes', NOTES_PAGE_SIZE: '20' });
const notes = defineConfig({
	debug: env.boolean('APP_DEBUG', false),
	pageSize: env.integer('NOTES_PAGE_SIZE', 20),
});
```

`defineConfig()` preserves inferred types and returns the same object. It does
not validate, freeze or clone it. Import configuration files explicitly; there
is no directory discovery or automatic merge order.

## Wire services explicitly

The shipped [runConfig.ts](./examples/runConfig.ts) passes the settings into
`new App({ config: { notes } })`. The repository becomes `application.config`.
It passes URL settings separately through the supported `url` option.
Putting a value under `config.url` would not configure the URL service.

Use the typed settings object directly where practical. At service boundaries:

```ts
const pageSize = application.config.get<number>('notes.pageSize');
const label = application.config.get('notes.missing', 'fallback');
const present = application.config.has('notes.webhook');
```

The generic in `get<number>()` is a TypeScript assertion, not runtime validation
or checked path autocomplete. See the [App/config guide](https://db3.ai/docs/app-config)
for the distinction between configuration data, service construction and process
startup.

## Presence and defaults

`get(path, fallback)` uses the fallback only when the property is absent.
Existing `null`, `undefined`, `false`, zero and empty strings are preserved.
`has()` checks property presence, not whether the value is truthy.

Paths traverse own object properties. They do not index arrays: retrieve the
whole array and work with it normally. Empty paths and empty path segments such
as `notes..title` throw. A missing intermediate object uses the fallback.

`all()` exposes the original object. Treat configuration as read-only, and never
return `all()` from an endpoint, log it or put it in browser hydration data.

## Environment parsers

| Reader | Behaviour |
| --- | --- |
| `env()` / `env.string()` | Raw string, including an empty string. Fallback applies only to `undefined`. |
| `env.required()` | Rejects missing or exactly empty values. It does not trim whitespace; add domain validation when needed. |
| `env.boolean()` | Case-insensitive `1/true/yes/on` or `0/false/no/off`; rejects other nonempty values. |
| `env.number()` | Converts to a finite number; rejects invalid or non-finite values. |
| `env.integer()` | Also rejects fractional values. Validate application ranges separately. |
| `env.array()` | Comma-separated, trimmed, nonempty entries. |
| `env.json<T>()` | Parses JSON, not a schema. `T` does not validate the parsed shape. |

Typed boolean/number/integer/array/JSON readers trim strings and return their
fallback for missing or empty values. With no fallback the result is
`undefined`. Invalid nonempty input throws rather than silently using a default.

`createEnv()` reads its supplied object on each call. The exported `env` reads
`process.env`. A constructed config object is a snapshot of the values read at
boot; changing environment variables does not recreate an initialized service.
Restart the process after changing settings.

## Testing and cleanup

The website's [Config guide](https://db3.ai/docs/config#testing) supplies the exact
three-test file. Save it as `tests/config/runConfig.test.ts`, keeping the copied
examples in `examples/`, then run:

```sh
npx vitest run tests/config/runConfig.test.ts
npx tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --types node --skipLibCheck examples/*.ts
```

It checks defaults, overrides, public output, malformed values, range limits,
optional credentials, fallback/presence and raw versus parsed values. The tests
do not mutate `process.env` or mock framework services. The runner closes App
in `finally`; Config itself owns no handles.

For framework maintainers, the service suite is
`npm run test:service --workspace packages/app -- config --maxWorkers=1`.

## Coverage and advanced reference

The walkthrough exercises typed boot settings, validation, App integration,
Config reuse, safe output and the common parsers. Existing service tests cover
the remaining malformed parser inputs. There is no automatic hot reload,
schema validation, config discovery or secret manager.

The [Config API reference](https://db3.ai/docs/config-api) renders the exact
emitted repository methods and environment-reader overloads from the staged
package. Application types and permissions remain in your app.
````

## Guidance for AI tools
Use the documented public import `@db3.ai/app/config` and its exported types. Prefer the source-backed examples and behavioural outcomes above over invented APIs or source-relative internal imports.
