Skip to content

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:

  1. Defaultsconfig.defaults.json loaded by the caller.
  2. Profileparams.profile, supplied by the caller (e.g., dev profile baked into the runtime).
  3. Providers — every entry in matrix.json's config.providers[], in declaration order.
  4. Overridesparams.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 varConfig path
MATRIX_CHAT_LOG_LEVEL=infologLevel
MATRIX_CHAT_AUTH__PROVIDER=googleauth.provider
MATRIX_CHAT_LIMITS__MAX_MESSAGES=200limits.maxMessages
MATRIX_CHAT_TIMEOUT_MS=5000timeoutMs
MATRIX_CHAT_LIMITS__MAX_MESSAGES_PER_HOUR=100limits.maxMessagesPerHour

Step by step:

  1. Strip the prefix (MATRIX_CHAT_).
  2. Split the remainder on __ (double underscore) — that's the path separator between segments.
  3. Within each segment, split on _, lowercase each part, and join camelCase: MAX_MESSAGESmaxMessages.
  4. Set the value at the resulting deep path.

Value parsing

Values are parsed (:303-318):

Raw env valueParsed
"true", "false", "null"boolean / null
"42", "-3.14"number
"{...}" or "[...]"JSON-parsed (falls back to raw string on parse error)
Anything elsestring (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=200

File at MATRIX_CHAT_CONFIG_FILE=/etc/chat.json:

json
{ "auth": { "provider": "google" } }

Service system.config.getEffective returns limits.timeoutMs = 30000.

Caller overrides: { logLevel: "warn" }.

LayerAfter 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

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