diff --git a/Cargo.lock b/Cargo.lock
index 733e73731c4..5235c0b64de 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2838,6 +2838,18 @@ version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+[[package]]
+name = "figment"
+version = "0.10.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
+dependencies = [
+ "atomic",
+ "serde",
+ "uncased",
+ "version_check",
+]
+
[[package]]
name = "filedescriptor"
version = "0.8.3"
@@ -3556,6 +3568,11 @@ dependencies = [
name = "hash-config"
version = "0.0.0"
dependencies = [
+ "error-stack",
+ "figment",
+ "serde",
+ "serde_core",
+ "serde_json",
"simple-mermaid",
]
@@ -10844,6 +10861,15 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
+[[package]]
+name = "uncased"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
+dependencies = [
+ "version_check",
+]
+
[[package]]
name = "unicase"
version = "2.9.0"
diff --git a/Cargo.toml b/Cargo.toml
index a05077f3adf..25b912d2fcc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -157,6 +157,7 @@ ena = { version = "0.14.3", default-features = fa
enum-iterator = { version = "2.1.0", default-features = false }
enumflags2 = { version = "0.7.12", default-features = false }
expect-test = { version = "1.5.1", default-features = false }
+figment = { version = "0.10.19", default-features = false }
foldhash = { version = "0.2.0", default-features = false }
frunk = { version = "0.4.4", default-features = false }
frunk_core = { version = "0.4.4", default-features = false }
diff --git a/libs/@local/config/Cargo.toml b/libs/@local/config/Cargo.toml
index 3aab908cc90..0405870c4ae 100644
--- a/libs/@local/config/Cargo.toml
+++ b/libs/@local/config/Cargo.toml
@@ -7,8 +7,24 @@ publish.workspace = true
version.workspace = true
[dependencies]
+# Public workspace dependencies
+error-stack = { workspace = true, public = true, features = ["std"] }
+
+# Public third-party dependencies
+serde_core = { workspace = true, public = true }
+
# Private third-party dependencies
+figment = { workspace = true }
simple-mermaid = { workspace = true }
+[dev-dependencies]
+serde = { workspace = true, features = ["derive"] }
+serde_json = { workspace = true }
+
+[[example]]
+doc-scrape-examples = true
+name = "defaults"
+test = true
+
[lints]
workspace = true
diff --git a/libs/@local/config/docs/dependency-diagram.mmd b/libs/@local/config/docs/dependency-diagram.mmd
index 40c3712b03f..d703870ca86 100644
--- a/libs/@local/config/docs/dependency-diagram.mmd
+++ b/libs/@local/config/docs/dependency-diagram.mmd
@@ -10,3 +10,5 @@ graph TD
%% ---> : Build dependency
0[hash-config]
class 0 root
+ 1[error-stack]
+ 0 --> 1
diff --git a/libs/@local/config/examples/defaults.rs b/libs/@local/config/examples/defaults.rs
new file mode 100644
index 00000000000..15eaccd3386
--- /dev/null
+++ b/libs/@local/config/examples/defaults.rs
@@ -0,0 +1,96 @@
+#![expect(clippy::print_stdout, clippy::use_debug)]
+//! Builds a store configuration from two layers of programmatic defaults.
+
+use error_stack::Report;
+use hash_config::{LoadError, Loader};
+use serde::{Deserialize, Serialize};
+use serde_json::json;
+
+#[derive(Debug, Deserialize)]
+struct Config {
+ store: Store,
+ routes: Vec,
+}
+
+#[derive(Debug, Deserialize)]
+struct Store {
+ host: String,
+ port: u16,
+}
+
+#[derive(Serialize)]
+struct StoreDefaults {
+ host: &'static str,
+ port: u16,
+}
+
+#[derive(Serialize)]
+struct Defaults {
+ store: StoreDefaults,
+ routes: [&'static str; 2],
+}
+
+/// The values the binary ships with.
+const SHIPPED: Defaults = Defaults {
+ store: StoreDefaults {
+ host: "localhost",
+ port: 5432,
+ },
+ routes: ["api", "health"],
+};
+
+/// A deployment moves the store to another port and leaves the rest alone.
+fn deployed() -> Result> {
+ Loader::new()
+ .with_defaults(SHIPPED)
+ .with_defaults(json!({ "store": { "port": 6543 } }))
+ .load()
+}
+
+/// A deployment sets the port to a password by mistake.
+fn misconfigured() -> Result> {
+ Loader::new()
+ .with_defaults(SHIPPED)
+ .with_defaults(json!({ "store": { "port": "hunter2" } }))
+ .load()
+}
+
+fn main() {
+ let config = deployed().expect("the deployed defaults should load");
+ println!(
+ "{}:{} serving {:?}",
+ config.store.host, config.store.port, config.routes
+ );
+
+ let report = misconfigured().expect_err("a password should not load as a port");
+ println!("\n{report:?}");
+}
+
+#[test]
+fn deployment_overrides_shipped_port() {
+ let config = deployed().expect("the deployed defaults should load");
+
+ assert_eq!(
+ config.store.host, "localhost",
+ "the shipped host should survive the deployment layer"
+ );
+ assert_eq!(
+ config.store.port, 6543,
+ "the deployment layer should replace the shipped port"
+ );
+}
+
+#[test]
+fn report_names_key_not_password() {
+ let report = misconfigured().expect_err("a password should not load as a port");
+ let rendered = format!("{report:?}");
+
+ assert!(
+ rendered.contains("store.port"),
+ "the report should name the key: {report:?}"
+ );
+ assert!(
+ !rendered.contains("hunter2"),
+ "the report should omit the value: {report:?}"
+ );
+}
diff --git a/libs/@local/config/package.json b/libs/@local/config/package.json
index 7ee0f892473..81a0f523343 100644
--- a/libs/@local/config/package.json
+++ b/libs/@local/config/package.json
@@ -8,5 +8,8 @@
"fix:clippy": "just clippy --fix",
"lint:clippy": "just clippy",
"test:unit": "mise run test:unit @rust/hash-config"
+ },
+ "dependencies": {
+ "@rust/error-stack": "workspace:*"
}
}
diff --git a/libs/@local/config/src/defaults.rs b/libs/@local/config/src/defaults.rs
new file mode 100644
index 00000000000..e9ab87a2807
--- /dev/null
+++ b/libs/@local/config/src/defaults.rs
@@ -0,0 +1,34 @@
+use figment::{
+ Metadata, Profile, Provider,
+ error::Error as FigmentError,
+ providers::Serialized,
+ value::{Dict, Map},
+};
+use serde_core::Serialize;
+
+/// The programmatic default layer.
+pub(crate) struct Defaults(Serialized);
+
+impl Defaults {
+ #[track_caller]
+ pub(crate) fn new(values: T) -> Self {
+ Self(Serialized::defaults(values))
+ }
+}
+
+impl Provider for Defaults
+where
+ T: Serialize,
+{
+ fn metadata(&self) -> Metadata {
+ let mut metadata = self.0.metadata();
+ // `Serialized` names itself after the Rust type it was handed.
+ metadata.name = "defaults".into();
+ // Figment's default notation prefixes the profile a key was found under.
+ metadata.interpolater(|_profile, keys| keys.join("."))
+ }
+
+ fn data(&self) -> Result