Skip to main content

Configuration sources

ConfigSource is an abstraction over the place a configuration is loaded from. In Phase 1 the primary source is YamlFileSource (files on disk), but the API is designed for extension. Phase 2+ adds adapters for centralized configuration stores:

SourceExampleStatus
YamlFileSourceapp-config.yaml on diskPhase 1 (default)
JsonFileSourceapp-config.json on diskPhase 1
InMemorySource (Python/TS) / DictSource (Go)A dict assembled programmatically — for testsPhase 1
Env interpolation ${VAR}Reads the environment while processing raw textPhase 1 (built in, not a separate source)
EtcdSourceKeys in etcd under a prefixPhase 2 (planned)
ConsulSourceKV store in ConsulPhase 2 (planned)
VaultSourceSecrets in HashiCorp VaultPhase 2 (planned)
HttpSourceA REST endpoint returning JSONPhase 2 (planned)
SqlSourceA database table with config that changes at runtimePhase 2 (planned)
KubernetesSourceA ConfigMap + Secret in KubernetesPhase 2 (planned)

The interface

Every source implements a minimal contract:

id: string # unique source identifier (e.g. "yaml:/app/config.yaml")
load(): ConfigTree # one-shot load (required)
watch?(callback): Subscription # subscribe to changes (optional)
close?(): void # release resources (optional)

load() returns a ConfigTree — a tree of values that the loader then merges with other sources and runs through env interpolation.

watch(callback) is optional. If the source supports watching (a file via fsnotify; etcd via a gRPC stream; Kubernetes via an informer), the loader passes a callback that is invoked when the source detects a change. If the source does not support it, the method is omitted; subscriptions for changes are still registered, but they activate with active = false.

The default scenario — YamlFileSource

For most dagstack applications, Config.load(path) is a high-level wrapper over Config.loadFrom([YamlFileSource(path), ...]). It automatically discovers and wires up three layers:

  1. app-config.yaml — base (committed).
  2. app-config.local.yaml — developer overrides (gitignored).
  3. app-config.${DAGSTACK_ENV}.yaml — environment-specific (committed).

Layer mechanics are documented on the Layers page.

Listing sources explicitly

When you need a non-standard order (for example, in tests) or a mix of source types, use Config.loadFrom:

from dagstack.config import Config, YamlFileSource, InMemorySource

config = Config.load_from([
YamlFileSource("app-config.yaml"),
InMemorySource({"database": {"pool_size": 5}}), # test override
])

:::note In-memory source name In Python and TypeScript the source is called InMemorySource; in Go it is DictSource. This is a historical divergence between the bindings; the semantics are identical (an in-memory tree, with no interpolation by default). :::

Argument order = priority order. The first source has the lowest priority; the last has the highest (it overrides the earlier ones).

Environment variable substitution

Env variables are not a separate ConfigSource, but a processing stage applied to values in the loaded ConfigTree. String values of the form ${VAR} or ${VAR:-default} are interpolated immediately after source.load(), before merging with other sources.

Details are on the Environment variable substitution page.

Watch semantics and hot reload

When the application subscribes to changes for its section through config.onSectionChange(path, callback), the loader registers a watcher on every source that supports watching:

  1. source.watch(on_source_change) is registered on every source.
  2. Any change in a source → the loader re-assembles the tree.
  3. The new version is validated (if subscribed sections have a schema).
  4. If validation passes, subscribers receive the new values.
  5. If validation fails, the entire reload is rolled back; subscribers are not notified (atomic rollback).

If no source supports watching, every subscription is registered with active = false. This is not an error: an application can still start up successfully without hot reload.

For details, see Hot reload (watch).

Implementing your own ConfigSource

The v1.0 contract pins down the signatures, but not the on-disk format. If you need a custom source (for example, ZooKeeper), implement the interface and pass an instance to Config.loadFrom:

from dagstack.config import ConfigSource, ConfigTree, Subscription

class ZookeeperSource(ConfigSource):
id = "zookeeper://zk.example.com/my-app"

def __init__(self, host: str, prefix: str):
self._client = zk_connect(host)
self._prefix = prefix

def load(self) -> ConfigTree:
raw = self._client.get_recursive(self._prefix)
return ConfigTree.from_dict(raw)

def watch(self, callback) -> Subscription:
# callback(event) when the tree changes
...

def close(self) -> None:
self._client.close()

See also