Appearance
Overrides
A package's effective configuration is defaults deep-merged with up to four overlay layers. This page covers each layer in order, the env-var translation rules, and how layering composes.
Layer order, exactly
resolveConfiguredPackageConfig (PackageConfigResolver.ts:88-105) applies these layers, in this order:
- Defaults —
config.defaults.jsonloaded by the caller. - Profile —
params.profile, supplied by the caller (e.g.,devprofile baked into the runtime). - Providers — every entry in
matrix.json'sconfig.providers[], in declaration order. - Overrides —
params.overrides, supplied by the caller last (e.g., CLI flags, test injections).
Each layer is a DeepPartial<TConfig>. Later layers win. Within a layer, deep-merge applies — nested objects are merged recursively, arrays and primitives are replaced wholesale.
Profile
The profile slot lets the caller bake a known configuration on top of defaults before any provider runs. Used by:
- The runner when running with a specific environment label (
dev,staging). - Test fixtures that want a known starting config without touching env vars.
ts
const config = await resolveConfiguredPackageConfig({
defaults,
profile: { logLevel: 'debug', limits: { timeoutMs: 1000 } },
contract: matrixJson.config,
});If you don't pass a profile, this layer is a no-op.
Provider 1 — env
json
{ "kind": "env", "prefix": "MATRIX_CHAT_" }Walks process.env (or whatever you pass as options.env), picks every key starting with prefix, and translates to a deep config path.
Translation rules
The translation lives at PackageConfigResolver.ts:135-153 and :319-327. The rules:
| Env var | Config path |
|---|---|
MATRIX_CHAT_LOG_LEVEL=info | logLevel |
MATRIX_CHAT_AUTH__PROVIDER=google | auth.provider |
MATRIX_CHAT_LIMITS__MAX_MESSAGES=200 | limits.maxMessages |
MATRIX_CHAT_TIMEOUT_MS=5000 | timeoutMs |
MATRIX_CHAT_LIMITS__MAX_MESSAGES_PER_HOUR=100 | limits.maxMessagesPerHour |
Step by step:
- Strip the prefix (
MATRIX_CHAT_). - Split the remainder on
__(double underscore) — that's the path separator between segments. - Within each segment, split on
_, lowercase each part, and join camelCase:MAX_MESSAGES→maxMessages. - Set the value at the resulting deep path.
Value parsing
Values are parsed (:303-318):
| Raw env value | Parsed |
|---|---|
"true", "false", "null" | boolean / null |
"42", "-3.14" | number |
"{...}" or "[...]" | JSON-parsed (falls back to raw string on parse error) |
| Anything else | string (un-trimmed) |
Single-env-var overlay
Alternative form: a single env var holding a JSON object:
json
{ "kind": "env", "env": "MATRIX_CHAT_CONFIG" }MATRIX_CHAT_CONFIG='{"logLevel":"debug","auth":{"provider":"google"}}' becomes the overlay verbatim. Useful when you want one env var that controls the whole config (Docker / Kubernetes pattern).
Provider 2 — file
json
{ "kind": "file", "env": "MATRIX_CHAT_CONFIG_FILE", "optional": true }Reads a JSON file. Path comes from process.env[provider.env]. The caller supplies a loadFileConfig callback (PackageConfigResolver.ts:177-180):
ts
options: {
loadFileConfig: async (path) => JSON.parse(await fs.promises.readFile(path, 'utf8')),
}The resolver does not do file I/O itself — it lets the caller decide (Node fs, a sandboxed reader, whatever). With optional: true, a missing env var or missing file is silent; with optional: false, it's an error.
You can also use a literal path:
json
{ "kind": "file", "path": "./config.local.json", "optional": true }Provider 3 — service
json
{
"kind": "service",
"service": "system.config",
"namespace": "@open-matrix/chat",
"env": "MATRIX_CHAT_CONFIG_SERVICE",
"optional": true
}Calls system.config.listForMount({ mount: namespace }) and system.config.getEffective({ mount, key }) for every key (PackageConfigResolver.ts:185-272). Used when configuration is centrally managed and pushed at runtime.
env lets you override the service mount per environment. realm (options.realm) is included on every getEffective call so the service can produce per-root values.
The caller can intercept service lookups by supplying options.loadServiceConfig — useful for testing.
Caller overrides — last
ts
overrides: { auth: { provider: 'github' } }Applied after every provider. Used by test injections, CLI flags, anything that should always win.
Worked layering example
json
// matrix.json config block
{
"schema": "./config/config.schema.json",
"defaults": "./config/config.defaults.json",
"providers": [
{ "kind": "env", "prefix": "MATRIX_CHAT_" },
{ "kind": "file", "env": "MATRIX_CHAT_CONFIG_FILE", "optional": true },
{ "kind": "service", "service": "system.config", "namespace": "@open-matrix/chat", "optional": true }
]
}json
// config.defaults.json
{ "logLevel": "info", "auth": { "provider": "anthropic" }, "limits": { "maxMessages": 50 } }Environment:
MATRIX_CHAT_LOG_LEVEL=debug
MATRIX_CHAT_LIMITS__MAX_MESSAGES=200File at MATRIX_CHAT_CONFIG_FILE=/etc/chat.json:
json
{ "auth": { "provider": "google" } }Service system.config.getEffective returns limits.timeoutMs = 30000.
Caller overrides: { logLevel: "warn" }.
| Layer | After this layer |
|---|---|
| Defaults | { logLevel: "info", auth: { provider: "anthropic" }, limits: { maxMessages: 50 } } |
| Profile (none) | unchanged |
env provider | { logLevel: "debug", auth: { provider: "anthropic" }, limits: { maxMessages: 200 } } |
file provider | { logLevel: "debug", auth: { provider: "google" }, limits: { maxMessages: 200 } } |
service provider | { logLevel: "debug", auth: { provider: "google" }, limits: { maxMessages: 200, timeoutMs: 30000 } } |
| Overrides | { logLevel: "warn", auth: { provider: "google" }, limits: { maxMessages: 200, timeoutMs: 30000 } } |
The result is what await resolveConfiguredPackageConfig({...}) returns.
Arrays and primitive replacement
Deep merge does NOT merge arrays. If your defaults set tags: ["a", "b"] and an override sets tags: ["c"], the result is tags: ["c"]. If you want concatenation, do it yourself in your runtime; do not expect the resolver to be smart.
The same applies to primitives: a later layer's primitive replaces, not merges.
Configuring a single env var to set everything
json
{ "kind": "env", "env": "MATRIX_CHAT_CONFIG" }Set MATRIX_CHAT_CONFIG='{"logLevel":"debug","limits":{"maxMessages":100}}' and the entire JSON object is the overlay. Convenient for Docker — one env var carries the whole config.
You can use both prefix-mode and single-env-var-mode in the same env provider; prefix mode merges first, then the single-env JSON layers on top within the same provider invocation (PackageConfigResolver.ts:155-163).
See also
- Config schema, Defaults, Secrets references, Config explain.
- Package config contract — the
configblock.
Source:
projects/matrix-3/packages/core/src/runtime/PackageConfigResolver.ts:88-105(resolution order),:135-205(each provider),:303-327(env value/segment translation),:65-76(deepMergeConfig).