Skip to main content

Configuration Sources

A source produces a partial config object. Multiple sources merge; the union is what your app sees.

The shapes below are the actual ConfigSource union from @omnitron-dev/titan/module/config:

type ConfigSource =
| IFileConfigSource
| IEnvironmentConfigSource
| IArgvConfigSource
| IObjectConfigSource
| IRemoteConfigSource;

Five built-in source types

File — JSON / YAML / TOML / INI / .env

{
type: 'file',
path: 'config/default.yaml',
format?: 'json' | 'yaml' | 'toml' | 'ini' | 'env', // auto-detected if omitted
encoding?: 'utf8',
transform?: (data) => data,
optional?: false,
}

Reads a file on startup. Format is auto-detected from extension if format is not specified.

The format type accepts 'toml' | 'ini', but the current ConfigLoaderService only parses json, yaml, env, and properties — passing toml/ini throws at load time. Use one of the supported formats until loader support lands.

What optional forgives

optional: true means the file may be ABSENT. It does not mean the file may be WRONG.

A missing path returns {} and the boot continues, which is the point of the option — a git-ignored config/local.yaml that most machines do not have. But a file that EXISTS and cannot be used — unparseable content, an unsupported format, a path that cannot be read — throws, optional or not.

The line is drawn where the loader already draws it: the existence check. A typo in an optional YAML file used to be skipped as readily as a missing one, so the application booted on defaults with nothing to indicate that the file it was told to read had been ignored. That is the failure optional is least able to afford, because the operator who wrote the file believes it is in effect.

The two cases are told apart in code — the loader marks the error malformed — rather than by matching the text of a message, so this does not change if the wording does.

Environment variables

{
type: 'env',
prefix?: 'APP_',
separator?: '__', // for nested paths
transform?: 'lowercase' | 'uppercase' | 'camelCase' | (key, value) => any,
}

Env vars matching prefix become config keys. APP_DATABASE__URL becomes database.url when separator: '__'.

Argv

{
type: 'argv',
prefix?: '--app-',
}

Reads command-line flags.

Inline object

{
type: 'object',
data: { port: 3000, debug: true },
}

Note data (not value). A literal object; useful for defaults that don't warrant a file.

Remote HTTP

{
type: 'remote',
url: 'https://config.internal/my-app',
headers?: { Authorization: 'Bearer …' },
timeout?: 5_000,
retry?: 3,
}

Fetches config from a URL. Periodic re-fetch is implementation- defined; check the ConfigLoaderService source for current behaviour.

Source ordering

Sources merge in the order listed (deep merge — later wins per leaf key):

sources: [
{ type: 'object', data: { db: { url: 'localhost', pool: { max: 10 } } } },
{ type: 'env', prefix: 'APP_' }, // APP_DB__URL=prod-db
]
// Result: { db: { url: 'prod-db', pool: { max: 10 } } }

You can also assign explicit priority on the base IConfigSource when natural order is awkward.

Conventional source layout

A common pattern:

ConfigModule.forRoot({
schema: AppConfigSchema,
sources: [
{ type: 'file', path: 'config/default.yaml' },
{ type: 'file', path: 'config/local.yaml', optional: true },
{ type: 'env', prefix: 'APP_' },
],
})
  • default.yaml — checked into git, sane defaults.
  • local.yaml — git-ignored, developer overrides.
  • APP_* env vars — production secrets, dynamic infra params.

Environment-specific files (production.yaml, etc.) can be loaded by adding an extra source whose path is computed at startup from your app's environment detection.

Custom sources

Provide your own loader by extending ConfigLoaderService or implementing IConfigSource plus a registration shim. The source interface and loader are in @omnitron-dev/titan/module/config/config-loader.service.ts.

Anti-patterns

  • Secrets in files committed to git. config/default.yaml should not contain production secrets. Use env vars or a secret manager.
  • Dynamic config paths from user input. A config-injection vulnerability. Use only static paths or values from trusted sources.
  • Big monolithic config. A 500-line default.yaml is hard to reason about. Split into per-domain files and load them individually.

→ Next: Validation.