Appearance
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 explainis the canonical command name. Today, the diagnostic surface is provided bysystem.config.getEffective(withsourcein the response) and tooling likehivecast doctorreads it. The command-line wrapper exists but its behaviour and flag set evolve; see@matrix/mx-clifor current capabilities.
The intent:
bash
matrix config explain @open-matrix/chatprints 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=200Then 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
| Symptom | Likely cause |
|---|---|
Setting X is undefined at runtime | Defaults missing it AND no override sets it. Add to defaults. |
| Setting X is the default in prod, not the env value | Env-var name wrong (typo, missing prefix, missing __ separator). |
| Setting X is wrong only on some Hosts | system.config.getEffective returning per-realm value. Inspect with matrix invoke. |
| Setting X jumps to a service value but never to the env override | Env provider declared AFTER service in providers[]. Reorder. |
Setting X is null when an env var is set | Raw 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 merged | Source layer's value type changed an object to a primitive (or vice versa). Deep-merge does object↔object only. |
| Array value isn't merged | Arrays 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
- Config schema — the JSON Schema in
config.schema. - Defaults, Overrides, Secrets references.
- Package config contract — the
configblock.
Source:
projects/matrix-3/packages/core/src/runtime/PackageConfigResolver.ts:88-105(resolution path),system.configactor for service-overlay introspection.