Skip to content

Package config contract

A package's configuration is layered. There are defaults baked into the package, profile overlays, environment-variable overlays, file overlays, and system.config service overlays. resolveConfiguredPackageConfig walks them in order and returns the merged result. This page documents the contract.

The config block

json
"config": {
  "schema": "./config/config.schema.json",
  "defaults": "./config/config.defaults.json",
  "envPrefix": "MATRIX_CHAT_",
  "providers": [
    { "kind": "env", "prefix": "MATRIX_CHAT_" },
    { "kind": "file", "env": "MATRIX_CHAT_CONFIG_FILE", "optional": true },
    { "kind": "service", "service": "system.config", "namespace": "@open-matrix/chat", "env": "MATRIX_CHAT_CONFIG_SERVICE", "optional": true }
  ]
}
FieldMeaning
schemaPath (package-relative) to a JSON Schema describing valid config. Used by tooling for validation, autocompletion, config explain.
defaultsPath to a JSON file containing the package's default config. The starting point for layering.
envPrefixIf providers is omitted, envPrefix implies a single env-provider with that prefix. Convenience for simple cases.
providers[]Ordered list of provider overlays applied on top of defaults. Later providers win for the same key.

The interface lives at projects/matrix-3/packages/core/src/runtime/PackageConfigResolver.ts:20-25.

Provider kinds

env — environment variables

json
{ "kind": "env", "prefix": "MATRIX_CHAT_" }

Walks process.env, picks up every key starting with prefix, and translates the suffix into a deep path:

Env varPathNotes
MATRIX_CHAT_LOG_LEVEL=debuglogLevelSingle segment.
MATRIX_CHAT_AUTH__PROVIDER=googleauth.provider__ is the separator between segments.
MATRIX_CHAT_LIMITS__MAX_MESSAGES=100limits.maxMessagesCamel-cased automatically.
MATRIX_CHAT_TIMEOUT_MS=5000timeoutMsNumber parsed if value matches /^-?\d+(\.\d+)?$/.
MATRIX_CHAT_FEATURES={"x":1}featuresJSON parsed if the value starts with { or [.
MATRIX_CHAT_DEBUG=truedebugtrue/false/null parsed as booleans/null.

The translation lives at PackageConfigResolver.ts:135-153 (resolveEnvOverlay) and :319-327 (envSegmentToPathSegment).

You can also point an env-provider at a single JSON object:

json
{ "kind": "env", "env": "MATRIX_CHAT_CONFIG" }

MATRIX_CHAT_CONFIG='{"logLevel":"debug","auth":{"provider":"google"}}' becomes the overlay verbatim.

file — JSON file pointed to by an env var

json
{ "kind": "file", "env": "MATRIX_CHAT_CONFIG_FILE", "optional": true }

Reads the path from process.env[provider.env], calls options.loadFileConfig(path), and uses the parsed object as an overlay. optional: true means "no env var set ⇒ skip this provider"; optional: false and missing file is an error.

The caller (typically the runner) supplies loadFileConfig; the resolver itself does no file I/O.

You can also use a literal path:

json
{ "kind": "file", "path": "./config.local.json", "optional": true }

service — fetched from system.config

json
{ "kind": "service", "service": "system.config", "namespace": "@open-matrix/chat", "optional": true }

Calls system.config.listForMount and system.config.getEffective for every key under the namespace. Used when configuration is centrally managed and pushed at runtime (per-user overrides, per-Space defaults, etc.).

Each getEffective call may include realm from options.realm — useful when configuration varies per authority root.

The custom resolver hook options.loadServiceConfig lets the caller intercept service lookups (testing, alternative providers).

Default order

resolveConfiguredPackageConfig (PackageConfigResolver.ts:88-105) layers in this exact order:

  1. defaults (the JSON loaded from config.defaults).
  2. profile (an explicit overlay passed by the caller — e.g., a "dev" profile baked into the runtime).
  3. For each provider in config.providers, in declaration order:
    • resolve overlay; deep-merge into the working result.
  4. overrides (an explicit overlay the caller layers last — e.g., test overrides, CLI flags).

Later layers win on conflict; deep-merge means nested objects are merged recursively, but arrays and primitives are replaced wholesale.

If providers is omitted but envPrefix is set, a single { kind: 'env', prefix: envPrefix } is implied (PackageConfigResolver.ts:107-117).

Worked example

json
"config": {
  "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 }
  ]
}

config.defaults.json:

json
{ "logLevel": "info", "auth": { "provider": "anthropic" }, "limits": { "maxMessages": 50 } }

Environment:

MATRIX_CHAT_LOG_LEVEL=debug
MATRIX_CHAT_LIMITS__MAX_MESSAGES=200

File (MATRIX_CHAT_CONFIG_FILE=/etc/matrix/chat.json):

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

Service (system.config.getEffective for @open-matrix/chat):

json
{ "config": [
  { "key": "limits.timeoutMs", "value": 30000 }
] }

Resolution:

  1. Start: { logLevel: "info", auth: { provider: "anthropic" }, limits: { maxMessages: 50 } }.
  2. Env overlay: logLevel: "debug", limits.maxMessages: 200{ logLevel: "debug", auth: { provider: "anthropic" }, limits: { maxMessages: 200 } }.
  3. File overlay: auth.provider: "google"{ logLevel: "debug", auth: { provider: "google" }, limits: { maxMessages: 200 } }.
  4. Service overlay: limits.timeoutMs: 30000{ logLevel: "debug", auth: { provider: "google" }, limits: { maxMessages: 200, timeoutMs: 30000 } }.

The resulting config is what your actor sees.

Calling the resolver

ts
import { resolveConfiguredPackageConfig } from '@open-matrix/core';

interface MyConfig {
  logLevel: 'debug' | 'info' | 'warn';
  auth: { provider: 'google' | 'anthropic' };
  limits: { maxMessages: number; timeoutMs?: number };
}

const config = await resolveConfiguredPackageConfig<MyConfig>({
  defaults,
  contract: matrixJson.config,
  packageName: '@open-matrix/chat',
  options: {
    context: matrixContext,        // for service provider
    env: process.env,
    loadFileConfig: async (path) => JSON.parse(await fs.promises.readFile(path, 'utf8')),
    timeoutMs: 5000,
  },
  overrides: cliFlagOverrides,
});

The return type is your declared config type, fully populated. The resolver does not validate against config.schema — that is the caller's job (typically the runner or matrix config explain).

Schema and validation

The config.schema path points to a JSON Schema. The SDK does not validate at config-resolution time. Validation is applied by:

  • matrix config explain <package> — the CLI reads the schema and the resolved config, prints which key came from which layer, and validates.
  • hivecast doctor — when run with --repair, checks each known package's resolved config against its schema.
  • Your actor — for safety, validate inside onBootstrap(). The defaults file should already pass; only the provider overlays can introduce invalid values.

See also

Source: projects/matrix-3/packages/core/src/runtime/PackageConfigResolver.ts:1-349 (full resolver). projects/matrix-3/packages/chat/matrix.json:135-156 (live config block).