Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Quick Start

use dynamic_config::dynamic_config;
use serde::Deserialize;

#[dynamic_config(
    files = ["config.toml", "secrets.json"],
    key   = "db",
    env   = "APP_",
    watch,
)]
#[derive(Debug, Deserialize)]
pub struct DatabaseConfig {
    pub host: String,
    pub port: u16,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    DatabaseConfig::init()?;        // load once, fail fast on a bad config
    DatabaseConfig::start_watch()?.detach(); // reload in the background from now on

    let config = DatabaseConfig::current();
    println!("{}:{}", config.host, config.port);

    Ok(())
}
[dependencies]
dynamic-config = { version = "0.1.0", features = ["toml", "watch"] }

Features

Every one of these is described in full in the chapters that follow; this is the map.

Loading

FormatsJSON, TOML, YAML — each behind its own feature, and using one that is off is a compile error naming it
Several files, mergedfiles = ["config.toml", "secrets.json"], left to right; a file that is not there is skipped, which is what makes an optional secrets.json work
Discoveryname = "config" with paths = ["/etc/myapp", "~/.config/myapp", "."]; ~ expands, and resolution happens per load so a file that appears later is picked up
Profilesprofile_env = "APP_ENV" layers config.production.toml over config.toml, for discovered and listed files alike
Encrypted filessecrets.json.age decrypts at load time; the suffix marks it, the extension under it names the format
.env filesenv_files = [".env"], read as the environment layer rather than as documents — and without touching the process environment
Any figment providerSource::provider(..) behind the figment feature, for Serialized::defaults(T), a custom Env, or one you wrote
No files at allfiles = [] for a container fed by a store and the environment

Layers

defaults < files < remote < .env < APP_DB_* < bind_env < flags < overrides
Environmentenv = "APP_" with configurable nesting (APP_DB_POOL__MAX_SIZE), and FOO= treated as unset unless you say otherwise
Named variablesbind_env("port", "PORT") for the ones you do not get to name — PORT, DATABASE_URL, REDIS_URL
Command lineset_flag, set_assignments(["k=v"]), and bind_clap behind a feature that takes only arguments that really came from the command line
Runtimeset_default below everything, set_override above it
Key aliasesalias("pool.size", "pool.max_size") keeps files written before a rename working, filling a gap rather than overriding
Tables merge, arrays replacea three-line secrets.json overrides two fields of a large config.toml; a list is never silently concatenated

Reading

Lock-freecurrent() is an atomic load — no mutex, no contention, callable per request
Snapshotsa reader holding an Arc keeps its own generation; a reload never mutates underneath it
Generic config typesDb<Postgres> and Db<Mysql> get separate snapshots, keyed by TypeId; non-generic types keep their static and pay nothing
Schema-less accesssnapshot() plus get, contains and sub, for the keys a struct does not name

Reloading

File watchingdirectory-level, so editor and mv-based atomic saves survive; Kubernetes ConfigMap updates are recognised
Poll fallbackpoll / poll_interval for NFS and overlay filesystems, where inotify registers and then silently delivers nothing
Debounceone editor save is several filesystem events
Remote storesetcd, Consul, NATS, Redis, Vault, S3 and Firestore — each watching the way its protocol allows
Hookson_reload(previous, current), and changes() for a task that would rather await
Any runtime, or nonechanges() is a Future over a generation counter and a list of wakers; tokio, smol and Embassy all drive it
All-or-nothingReloadGroup prepares every member before any of them commits
Key-level diffsdiff logs which keys moved — paths only, never values

Safety

Validationvalidate runs your own check on every load; a reload that fails it keeps the previous snapshot
A bad reload cannot take the process downthe running snapshot stays until a new one is complete and valid
Secret redaction#[config(secret)] prints ***, and #[derive(Debug)] alongside it is a compile error rather than a race between two impls
Nothing leaks a valuediffs, check() reports, unknown-key suggestions and error messages all report paths and types, never values
Files written are privatesave and the cache create their file 0600 and refuse to follow a symlink planted at the temporary path
Writing without replacingsave_new refuses if the file exists, for a setup wizard that must not overwrite what somebody wrote
Writing encryptedsave_encrypted to a recipient list, the counterpart to reading a secrets.json.age
Last known goodcache starts from yesterday's configuration when today's is broken, in three modes so what lands on disk is a choice

Diagnostics

Provenance in every errorpool.max_size: invalid type: found a string, expected u16 (from APP_DB_)
source_of / is_setwhich layer supplies a key, and whether anything does
check()what the configuration resolves to, without loading it — works when the load fails, which is when it is worth running
Unknown keyswith suggestions from a transposition-aware edit distance, so prot finds port
A JSON Schemaschema() describes the file, marks secrets writeOnly, and drops required because a file is one layer of six