Skip to content

Config explain

When configuration is layered, debugging "why is logLevel 'warn' here?" needs more than console.log. The intended tool is matrix config explain <package>, which walks each layer and prints which key came from which provider. This page covers the command, what it shows, and the patterns for diagnosing config issues from your actor when the CLI isn't available.

The CLI command

Status: target state, partially implemented. matrix config explain is the canonical command name. Today, the diagnostic surface is provided by system.config.getEffective (with source in the response) and tooling like hivecast doctor reads it. The command-line wrapper exists but its behaviour and flag set evolve; see @matrix/mx-cli for current capabilities.

The intent:

bash
matrix config explain @open-matrix/chat

prints a per-key breakdown:

@open-matrix/chat — effective configuration

logLevel        = "debug"          [env: MATRIX_CHAT_LOG_LEVEL]
auth.provider   = "google"         [file: /etc/matrix/chat.json]
limits.maxMessages = 200           [env: MATRIX_CHAT_LIMITS__MAX_MESSAGES]
limits.timeoutMs   = 30000         [service: system.config @ @open-matrix/chat]

Each row shows the resolved value and, in brackets, which layer supplied it. Validation errors against config.schema.json appear at the bottom.

Diagnosing layering issues from an actor

When the CLI isn't on hand (running inside a container, debugging a test failure), the patterns below let you inspect the configuration state directly.

1. Log the resolved config (briefly) at bootstrap

ts
class MyRoot extends MatrixActor {
  protected async onBootstrap() {
    this.config = await loadConfig();
    actorLog('boot', 'effective config', { config: this.config });
  }
}

actorLog (the bus-native logger) writes to the runtime's record store and the console (console.log is forbidden by project standards). Print the full config once at boot — you almost never need it during runtime.

Caution: Do NOT log configuration that contains secrets. With the Secrets references discipline, configs should not contain secrets, but be paranoid: filter known sensitive keys before logging.

2. Check system.config.getEffective directly

When you suspect a service overlay is mis-set:

bash
matrix invoke system.config config.getEffective '{"mount":"@open-matrix/chat","key":"limits.timeoutMs"}'

The response includes value and source (or whatever the service implementation provides). Useful for checking what the service layer thinks is true without resolving the whole config.

3. Walk env vars manually

If the env-var translation is producing unexpected paths:

bash
env | grep MATRIX_CHAT_
# MATRIX_CHAT_LOG_LEVEL=debug
# MATRIX_CHAT_LIMITS__MAX_MESSAGES=200

Then mentally apply the rules from Overrides — strip prefix, split on __, camelCase each segment.

4. Add a test that pins behaviour

The most durable form of "config explain" is a test:

ts
import { resolveConfiguredPackageConfig } from '@open-matrix/core';
import defaults from '../../config/config.defaults.json';
import contract from '../../matrix.json' assert { type: 'json' };

test('env override beats default', async () => {
  const config = await resolveConfiguredPackageConfig({
    defaults,
    contract: contract.config,
    options: { env: { MATRIX_CHAT_LOG_LEVEL: 'debug' } },
  });
  expect(config.logLevel).toBe('debug');
});

When a config issue ships to prod, write the regression test first.

Common diagnostic patterns

SymptomLikely cause
Setting X is undefined at runtimeDefaults missing it AND no override sets it. Add to defaults.
Setting X is the default in prod, not the env valueEnv-var name wrong (typo, missing prefix, missing __ separator).
Setting X is wrong only on some Hostssystem.config.getEffective returning per-realm value. Inspect with matrix invoke.
Setting X jumps to a service value but never to the env overrideEnv provider declared AFTER service in providers[]. Reorder.
Setting X is null when an env var is setRaw value was the literal string "null" — translation parses it. Use a different sentinel or quote: MATRIX_X='"null"'.
Object value is wholesale-replaced instead of mergedSource layer's value type changed an object to a primitive (or vice versa). Deep-merge does object↔object only.
Array value isn't mergedArrays are replaced wholesale by design. There is no concat mode.

Validating after resolution

The resolver does not validate. The right place to run validation is at the top of onBootstrap():

ts
import Ajv from 'ajv';
import schema from '../../config/config.schema.json';

const validate = new Ajv().compile(schema);

class MyRoot extends MatrixActor {
  protected async onBootstrap() {
    this.config = await loadConfig();
    if (!validate(this.config)) {
      throw new Error(`Invalid config: ${JSON.stringify(validate.errors)}`);
    }
  }
}

Failing fast at boot is much better than failing partway through a request because a typo caused a numeric field to be "twenty".

Tooling that reads config.schema

matrix config explain and hivecast doctor --repair both read the schema (target state for --repair). Editor tooling reads it via $schema in config.defaults.json. Your test code should read it too — see Config schema.

See also

Source: projects/matrix-3/packages/core/src/runtime/PackageConfigResolver.ts:88-105 (resolution path), system.config actor for service-overlay introspection.