<div align="center">
# dynamic-config
**Hot-reloadable, layered configuration for Rust — one attribute, lock-free reads.**
[](https://github.com/ctolon/dynamic-config/actions/workflows/ci.yml)
[](https://github.com/ctolon/dynamic-config/actions/workflows/security.yml)
[](https://crates.io/crates/dynamic-config)
[](https://docs.rs/dynamic-config)
[](https://ctolon.github.io/dynamic-config/msrv-features.html)
[](LICENSE)
[](https://scorecard.dev/viewer/?uri=github.com/ctolon/dynamic-config)
[**The Book**](https://ctolon.github.io/dynamic-config/) · [API docs](https://docs.rs/dynamic-config) · [Examples](https://github.com/ctolon/dynamic-config/tree/main/dynamic-config/examples) · [Changelog](CHANGELOG.md)
</div>
---
Configuration that stays live after startup: files, environment, remote
stores and command-line flags merged into one typed struct, re-read when
they change, served to every thread as one atomic load.
```toml
[dependencies]
dynamic-config = { version = "0.1.0", features = ["toml", "watch"] }
```
```rust
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(); // one atomic load, on any thread
println!("{}:{}", config.host, config.port);
Ok(())
}
```
## Why this one
- **Reads are lock-free.** `current()` is an atomic pointer load — ~17 ns —
so configuration can be read per request without a second thought.
- **A bad edit cannot take the process down.** A file that no longer parses
or validates degrades to "no change"; the previous snapshot keeps serving,
and the error is reported.
- **Layers with provenance.** `defaults < discovered < files < remote <
.env < environment < bindings < flags < overrides` — and `source_of("key")`
names the file, variable or store a value actually came from.
- **Secrets stay out of diagnostics.** Errors, diffs, reports and `{:?}`
print paths and types, never values — enforced by its own test suite.
- **Any runtime, or none.** The async surface is a `Future` and a thread;
tokio, smol and Embassy all drive it. Blocking work never lands on your
executor.
- **Remote stores are explicit.** `refresh_remote()` does the network round
trip; `load()` never does. Seven store crates ship, each watching the way
its protocol allows.
The full story — precedence, profiles, discovery, hot reload, encryption,
schema export, units, the last-known-good cache, testing patterns — lives in
[**the book**](https://ctolon.github.io/dynamic-config/).
## The workspace
| [`dynamic-config`](https://crates.io/crates/dynamic-config) | the engine: loading, layers, storage, watching | **Beta** |
| [`dynamic-config-macros`](https://crates.io/crates/dynamic-config-macros) | `#[dynamic_config]` | **Beta** |
| [`dynamic-config-etcd`](dynamic-config-etcd) | etcd, push watch over gRPC | Experimental |
| [`dynamic-config-consul`](dynamic-config-consul) | Consul KV, blocking queries | Experimental |
| [`dynamic-config-nats`](dynamic-config-nats) | NATS JetStream KV, push watch | Experimental |
| [`dynamic-config-redis`](dynamic-config-redis) | Redis, keyspace notifications | Experimental |
| [`dynamic-config-vault`](dynamic-config-vault) | Vault KV v2, version polling | Experimental |
| [`dynamic-config-s3`](dynamic-config-s3) | S3 & compatibles, ETag polling — needs tokio | Experimental |
| [`dynamic-config-firestore`](dynamic-config-firestore) | Firestore REST, `updateTime` polling | Experimental |
| [`dynamic-config-embedded`](dynamic-config-embedded) | the same shape for `no_std` targets | Experimental |
**Beta**: breaking changes bump the minor pre-1.0 and are announced in the
changelog. **Experimental**: may change shape without ceremony — pin an
exact version. Details in
[Stability Tiers](https://ctolon.github.io/dynamic-config/stability-tiers.html).
Every store follows the same contract — the current value is not announced
at startup, a deleted key is not a change, transport failures retry, a
panicking callback ends the watch with an error — and each documents its
stop latency and change-detection rule side by side in
[Store Crates at a Glance](https://ctolon.github.io/dynamic-config/remote-stores/store-crates.html).
## MSRV
| `dynamic-config` core | **1.71** |
| `schema` feature | 1.74 (schemars) |
| `watch` / `age` / `full` features | 1.85 (measured, not declared) |
| store crates | 1.85 — nats/redis/s3: 1.88 (their clients) |
| `dynamic-config-embedded` | 1.83 |
MSRV changes are breaking. Every floor has a CI row against a real
toolchain; the full table with reasons is in
[MSRV & Features](https://ctolon.github.io/dynamic-config/msrv-features.html).
## Contributing
[CONTRIBUTING.md](CONTRIBUTING.md) is the short version;
[the onboarding tour](docs/CONTRIBUTOR-ONBOARDING.md) walks every module.
What will *not* be built, and why, is in
[Limitations & Not Planned](https://ctolon.github.io/dynamic-config/limitations.html);
what might be is in [ROADMAP.md](ROADMAP.md).
## License
[MIT](LICENSE).