Appearance
Defaults
The config.defaults.json file is the bottom of the layering stack. It is the configuration your package runs with on a fresh install with no environment variables, no config file, and no system.config overrides set. This page covers what to put in it, what to keep out, and how it gets loaded.
The file
json
{
"logLevel": "info",
"auth": { "provider": "anthropic" },
"limits": { "maxMessages": 50, "timeoutMs": 30000 }
}Plain JSON. Lives at the path matrix.json's config.defaults points to (conventionally ./config/config.defaults.json). Loaded as the seed by resolveConfiguredPackageConfig (PackageConfigResolver.ts:88-105).
What goes in defaults
| Goes in defaults | Doesn't |
|---|---|
| Sensible production values (log level, request timeouts, cache sizes). | Per-deployment values (database URLs, API hostnames). |
| Built-in feature flags (default model, default theme). | Per-user values (preferences). |
| Hard-coded enum picks (default provider). | Secrets — credentials, API keys, tokens. |
Reasonable limits (maxMessages: 50, maxConcurrent: 5). | Per-installation IDs. |
The mental model: "if a user installs this package and runs it without touching anything, what should it do?" Whatever the answer is, that's defaults.
What does NOT go in defaults
Secrets
Anything sensitive — API keys, passwords, OAuth tokens, NATS credentials — never goes in config.defaults.json. The defaults file is committed to the repo, included in the package, visible to everyone who reads the source.
Credential references go through Factotum. See Secrets references.
Environment-specific values
Values that change between dev/stage/prod (database URLs, hostnames, port numbers) are overrides, not defaults. The defaults should be the most-portable configuration possible. If a value can't run as-is on a fresh install, it shouldn't be in defaults.
A reasonable rule: defaults should run on a developer's laptop with no setup. If your package needs a Postgres URL, the default should be postgres://localhost:5432/<sane-name> or the package should fail loudly if the URL is missing — never an internal-stage URL hard-coded into defaults.
User preferences
Per-user config (preferred theme, default font size, last-opened file) lives in system.preferences, not in config.defaults. Defaults are per-package globals; preferences are per-user.
Validation
The defaults file MUST validate against config.schema.json. If it doesn't, your package is broken on every fresh install — the very first layer of resolution produces invalid config. CI should validate this; for additive safety, your onBootstrap() should also validate.
Hot-reload / runtime updates
Defaults are baked in at package install time. Editing config.defaults.json after the fact requires a re-install (or, in dev, a runtime restart). Runtime tunables go through env vars or system.config instead — those are evaluated each time the resolver runs.
What the resolver does with defaults
resolveConfiguredPackageConfig (PackageConfigResolver.ts:88-105):
- Calls
deepMergeConfig(defaults, profile). - Applies provider overlays one at a time.
- Applies caller-supplied overrides last.
deepMergeConfig (:65-76):
ts
function deepMergeConfig<T extends object>(base: T, patch: DeepPartial<T> | undefined): T {
if (!patch) return structuredClone(base);
const output = structuredClone(base) as Record<string, unknown>;
for (const [key, value] of Object.entries(patch)) {
if (isRecord(value) && isRecord(output[key])) {
output[key] = deepMergeConfig(output[key], value);
continue;
}
output[key] = value;
}
return output as T;
}structuredClone ensures defaults are never mutated. Recursively merges nested objects; replaces arrays and primitives wholesale.
Conventions
- Stable JSON. No comments (JSON doesn't allow them), no trailing commas. If you need comments, put them next to the config schema's field
descriptioninstead. - Keep it small. A defaults file with hundreds of keys signals over-configuration. Most packages have under 20 settings.
- Order keys deliberately. Group related fields. The file is read by humans during operations.
- Reference the schema. Tooling autocompletes if your defaults file declares
"$schema": "./config.schema.json".
json
{
"$schema": "./config.schema.json",
"logLevel": "info",
"limits": { "maxMessages": 50 }
}Checking defaults validate
A trivial CI step:
bash
npx ajv validate -s config/config.schema.json -d config/config.defaults.jsonOr in tests:
ts
import Ajv from 'ajv';
import schema from '../../config/config.schema.json';
import defaults from '../../config/config.defaults.json';
test('defaults validate against schema', () => {
const validate = new Ajv().compile(schema);
if (!validate(defaults)) {
throw new Error(`Defaults invalid: ${JSON.stringify(validate.errors)}`);
}
});See also
- Config schema — what defaults must conform to.
- Overrides — what runs on top of defaults.
- Secrets references — what does NOT go in defaults.
Source:
projects/matrix-3/packages/core/src/runtime/PackageConfigResolver.ts:65-105(deepMergeConfig,resolveConfiguredPackageConfig).