You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iggy.apache.org by hg...@apache.org on 2026/01/27 15:21:47 UTC

(iggy) branch master updated: refactor(server): compile-time env var mappings eliminate runtime ambiguity (#2614)

This is an automated email from the ASF dual-hosted git repository.

hgruszecki pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git


The following commit(s) were added to refs/heads/master by this push:
     new 052431382 refactor(server): compile-time env var mappings eliminate runtime ambiguity (#2614)
052431382 is described below

commit 05243138255349a78bd1e086a0d7fb264682f980
Author: Hubert Gruszecki <h....@gmail.com>
AuthorDate: Tue Jan 27 16:21:41 2026 +0100

    refactor(server): compile-time env var mappings eliminate runtime ambiguity (#2614)
    
    Replace dynamic path inference with derive macro that generates
    explicit mappings at compile time. Field attribute #[config_env(leaf)]
    gives users control over nested vs value type handling without
    hardcoding type names in the macro.
    
    Added 2 new crates: `configs` and `configs_derive` which are meant
    to not be published to crates.io.
    
    Renamed `server.toml` to `config.toml` and moved it to `core/server`.
---
 .github/config/components.yml                      |  54 +-
 Cargo.lock                                         | 178 ++--
 Cargo.toml                                         |   8 +
 DEPENDENCIES.md                                    |  60 +-
 README.md                                          |   8 +-
 bdd/docker-compose.yml                             |  34 +-
 core/ai/mcp/Cargo.toml                             |   3 +-
 core/ai/mcp/src/configs.rs                         |  39 +-
 core/ai/mcp/src/main.rs                            |   2 +-
 core/common/Cargo.toml                             |   3 +-
 core/common/src/configs/mod.rs                     | 893 ---------------------
 core/common/src/lib.rs                             |   3 +-
 core/configs/Cargo.toml                            |  30 +
 core/configs/src/configs_impl/env_mapping.rs       |  63 ++
 core/configs/src/configs_impl/error.rs             |  51 ++
 core/configs/src/configs_impl/file_provider.rs     | 142 ++++
 core/configs/src/configs_impl/mod.rs               |  41 +
 core/configs/src/configs_impl/parsing.rs           | 196 +++++
 core/configs/src/configs_impl/traits.rs            |  36 +
 .../configs/src/configs_impl/typed_env_provider.rs | 597 ++++++++++++++
 core/configs/src/lib.rs                            |  28 +
 core/configs_derive/Cargo.toml                     |  32 +
 core/configs_derive/src/config_env.rs              | 583 ++++++++++++++
 core/configs_derive/src/lib.rs                     | 120 +++
 core/connectors/runtime/Cargo.toml                 |   2 +
 core/connectors/runtime/src/api/config.rs          |   8 +-
 core/connectors/runtime/src/configs/connectors.rs  |  17 +-
 .../src/configs/connectors/local_provider.rs       |  50 +-
 core/connectors/runtime/src/configs/runtime.rs     |  53 +-
 core/connectors/runtime/src/main.rs                |   2 +-
 core/integration/Cargo.toml                        |   1 +
 core/integration/src/test_connectors_runtime.rs    |   5 -
 core/integration/src/test_server.rs                |   3 +-
 core/integration/tests/config_provider/mod.rs      |  23 +-
 .../data_integrity/verify_after_server_restart.rs  |   2 +-
 core/server/Cargo.toml                             |   1 +
 core/server/README.md                              |   2 +-
 core/{configs/server.toml => server/config.toml}   |   0
 core/server/src/args.rs                            |   4 +-
 core/server/src/configs/cluster.rs                 |  11 +-
 core/server/src/configs/defaults.rs                |   4 +-
 core/server/src/configs/http.rs                    |  17 +-
 core/server/src/configs/quic.rs                    |  15 +-
 core/server/src/configs/server.rs                  |  83 +-
 core/server/src/configs/sharding.rs                |   4 +-
 core/server/src/configs/system.rs                  |  43 +-
 core/server/src/configs/tcp.rs                     |  11 +-
 core/server/src/configs/validators.rs              |  39 +-
 core/server/src/configs/websocket.rs               |   5 +-
 core/server/src/server_error.rs                    |   2 +-
 examples/java/README.md                            |   2 +-
 examples/rust/README.md                            |   2 +-
 foreign/node/src/e2e/tcp.cluster.e2e.ts            |   2 +-
 53 files changed, 2390 insertions(+), 1227 deletions(-)

diff --git a/.github/config/components.yml b/.github/config/components.yml
index 8a87480ee..6ea37faaa 100644
--- a/.github/config/components.yml
+++ b/.github/config/components.yml
@@ -37,10 +37,17 @@ components:
     paths:
       - "core/sdk/**"
 
+  rust-configs:
+    depends_on:
+      - "rust-workspace"
+    paths:
+      - "core/configs/**"
+      - "core/configs_derive/**"
+
   rust-common:
     depends_on:
-      - "rust-workspace" # Common is affected by workspace changes
-      - "ci-infrastructure" # CI changes trigger full regression
+      - "rust-workspace"
+      - "rust-configs"
     paths:
       - "core/common/**"
 
@@ -51,28 +58,52 @@ components:
     paths:
       - "core/binary_protocol/**"
 
+  rust-clock:
+    depends_on:
+      - "rust-workspace"
+    paths:
+      - "core/clock/**"
+
+  rust-journal:
+    depends_on:
+      - "rust-workspace"
+    paths:
+      - "core/journal/**"
+
+  rust-partitions:
+    depends_on:
+      - "rust-workspace"
+    paths:
+      - "core/partitions/**"
+
   rust-server:
     depends_on:
-      - "rust-workspace" # Server is affected by workspace changes
-      - "ci-infrastructure" # CI changes trigger full regression
+      - "rust-workspace"
+      - "rust-configs"
       - "rust-common"
       - "rust-binary-protocol"
+      - "rust-journal"
+      - "rust-partitions"
     paths:
       - "core/server/**"
 
   rust-cluster:
     depends_on:
-      - "rust-workspace" # Cluster is affected by workspace changes
-      - "ci-infrastructure" # CI changes trigger full regression
+      - "rust-workspace"
+      - "rust-clock"
     paths:
-      - "core/consensus/"
-      - "core/metadata/"
-      - "core/message_bus/"
+      - "core/consensus/**"
+      - "core/metadata/**"
+      - "core/message_bus/**"
 
   # Main Rust workspace testing
   rust:
     depends_on:
       - "rust-workspace"
+      - "rust-configs"
+      - "rust-clock"
+      - "rust-journal"
+      - "rust-partitions"
       - "rust-sdk"
       - "rust-common"
       - "rust-binary-protocol"
@@ -84,7 +115,7 @@ components:
       - "rust-connectors"
       - "rust-mcp"
       - "rust-integration"
-      - "ci-infrastructure" # CI changes trigger full regression
+      - "ci-infrastructure"
     paths:
       - "Dockerfile*"
       - "!foreign/**" # Exclude foreign SDKs
@@ -121,6 +152,7 @@ components:
   rust-connectors:
     depends_on:
       - "rust-sdk"
+      - "rust-configs"
     paths:
       - "core/connectors/**"
 
@@ -128,6 +160,7 @@ components:
   rust-mcp:
     depends_on:
       - "rust-sdk"
+      - "rust-configs"
     paths:
       - "core/ai/mcp/**"
 
@@ -138,6 +171,7 @@ components:
       - "rust-common"
       - "rust-binary-protocol"
       - "rust-server"
+      - "rust-configs"
     paths:
       - "core/integration/**"
 
diff --git a/Cargo.lock b/Cargo.lock
index 4d342f57b..e87d41682 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -893,9 +893,9 @@ dependencies = [
 
 [[package]]
 name = "aws-lc-rs"
-version = "1.15.4"
+version = "1.15.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256"
+checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288"
 dependencies = [
  "aws-lc-sys",
  "zeroize",
@@ -903,9 +903,9 @@ dependencies = [
 
 [[package]]
 name = "aws-lc-sys"
-version = "0.37.0"
+version = "0.35.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a"
+checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1"
 dependencies = [
  "cc",
  "cmake",
@@ -1317,7 +1317,7 @@ version = "3.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "89ec27229c38ed0eb3c0feee3d2c1d6a4379ae44f418a29a658890e062d8f365"
 dependencies = [
- "darling 0.20.11",
+ "darling 0.23.0",
  "ident_case",
  "prettyplease",
  "proc-macro2",
@@ -1498,9 +1498,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.2.54"
+version = "1.2.52"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583"
+checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3"
 dependencies = [
  "find-msvc-tools",
  "jobserver",
@@ -1665,7 +1665,7 @@ version = "3.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
 dependencies = [
- "windows-sys 0.48.0",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -1914,6 +1914,27 @@ dependencies = [
  "crossbeam-utils",
 ]
 
+[[package]]
+name = "configs"
+version = "0.1.0"
+dependencies = [
+ "configs_derive",
+ "figment",
+ "serde",
+ "serde_json",
+ "tracing",
+]
+
+[[package]]
+name = "configs_derive"
+version = "0.1.0"
+dependencies = [
+ "darling 0.20.11",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+]
+
 [[package]]
 name = "consensus"
 version = "0.1.0"
@@ -2665,7 +2686,7 @@ dependencies = [
  "libc",
  "option-ext",
  "redox_users",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -2969,7 +2990,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
 dependencies = [
  "libc",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -3160,20 +3181,21 @@ dependencies = [
 
 [[package]]
 name = "filetime"
-version = "0.2.27"
+version = "0.2.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
+checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed"
 dependencies = [
  "cfg-if",
  "libc",
  "libredox",
+ "windows-sys 0.60.2",
 ]
 
 [[package]]
 name = "find-msvc-tools"
-version = "0.1.8"
+version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
+checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41"
 
 [[package]]
 name = "flatbuffers"
@@ -4521,6 +4543,8 @@ dependencies = [
  "axum-server",
  "chrono",
  "clap",
+ "configs",
+ "configs_derive",
  "dashmap",
  "derive_more",
  "dirs",
@@ -4566,12 +4590,13 @@ version = "0.2.1-edge.5"
 dependencies = [
  "axum",
  "axum-server",
+ "configs",
+ "configs_derive",
  "dirs",
  "dotenvy",
  "figlet-rs",
  "figment",
  "iggy",
- "iggy_common",
  "opentelemetry",
  "opentelemetry-appender-tracing",
  "opentelemetry-otlp",
@@ -4631,11 +4656,11 @@ dependencies = [
  "compio-quic",
  "compio-tls",
  "compio-ws",
+ "configs_derive",
  "crossbeam",
  "derive_more",
  "err_trail",
  "fast-async-mutex",
- "figment",
  "human-repr",
  "humantime",
  "nix 0.31.1",
@@ -4650,7 +4675,6 @@ dependencies = [
  "strum",
  "thiserror 2.0.18",
  "tokio",
- "toml 0.9.11+spec-1.1.0",
  "tracing",
  "tungstenite",
  "twox-hash",
@@ -4966,6 +4990,7 @@ dependencies = [
  "bytes",
  "chrono",
  "compio",
+ "configs",
  "ctor",
  "derive_more",
  "env_logger",
@@ -5088,7 +5113,7 @@ dependencies = [
  "portable-atomic",
  "portable-atomic-util",
  "serde_core",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -5155,9 +5180,9 @@ version = "0.1.0"
 
 [[package]]
 name = "js-sys"
-version = "0.3.85"
+version = "0.3.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
+checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
 dependencies = [
  "once_cell",
  "wasm-bindgen",
@@ -5390,9 +5415,9 @@ dependencies = [
 
 [[package]]
 name = "liblzma-sys"
-version = "0.4.5"
+version = "0.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f2db66f3268487b5033077f266da6777d057949b8f93c8ad82e441df25e6186"
+checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736"
 dependencies = [
  "cc",
  "libc",
@@ -5401,9 +5426,9 @@ dependencies = [
 
 [[package]]
 name = "libm"
-version = "0.2.16"
+version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
 
 [[package]]
 name = "libmimalloc-sys"
@@ -5861,12 +5886,9 @@ dependencies = [
 
 [[package]]
 name = "notify-types"
-version = "2.1.0"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a"
-dependencies = [
- "bitflags 2.10.0",
-]
+checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
 
 [[package]]
 name = "nougat"
@@ -5904,7 +5926,7 @@ version = "0.50.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
 dependencies = [
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -5959,9 +5981,9 @@ dependencies = [
 
 [[package]]
 name = "num-conv"
-version = "0.2.0"
+version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
 
 [[package]]
 name = "num-integer"
@@ -6198,9 +6220,9 @@ dependencies = [
 
 [[package]]
 name = "openssl-probe"
-version = "0.2.1"
+version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391"
 
 [[package]]
 name = "openssl-src"
@@ -6360,7 +6382,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
 dependencies = [
  "libc",
- "windows-sys 0.48.0",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -6893,9 +6915,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.106"
+version = "1.0.105"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
 dependencies = [
  "unicode-ident",
 ]
@@ -7122,14 +7144,14 @@ dependencies = [
  "once_cell",
  "socket2 0.5.10",
  "tracing",
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
 ]
 
 [[package]]
 name = "quote"
-version = "1.0.44"
+version = "1.0.43"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
+checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
 dependencies = [
  "proc-macro2",
 ]
@@ -7736,7 +7758,7 @@ dependencies = [
  "errno",
  "libc",
  "linux-raw-sys",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -7778,9 +7800,9 @@ dependencies = [
 
 [[package]]
 name = "rustls-pki-types"
-version = "1.14.0"
+version = "1.13.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
+checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282"
 dependencies = [
  "web-time",
  "zeroize",
@@ -7804,7 +7826,7 @@ dependencies = [
  "security-framework",
  "security-framework-sys",
  "webpki-root-certs",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -7815,9 +7837,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
 
 [[package]]
 name = "rustls-webpki"
-version = "0.103.9"
+version = "0.103.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
+checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
 dependencies = [
  "aws-lc-rs",
  "ring",
@@ -7990,7 +8012,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521"
 dependencies = [
  "libc",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -8234,6 +8256,7 @@ dependencies = [
  "compio-quic",
  "compio-tls",
  "compio-ws",
+ "configs",
  "ctrlc",
  "cyper",
  "cyper-axum",
@@ -8948,7 +8971,7 @@ dependencies = [
  "getrandom 0.3.4",
  "once_cell",
  "rustix",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -9112,9 +9135,9 @@ dependencies = [
 
 [[package]]
 name = "time"
-version = "0.3.46"
+version = "0.3.45"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5"
+checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd"
 dependencies = [
  "deranged",
  "itoa",
@@ -9129,15 +9152,15 @@ dependencies = [
 
 [[package]]
 name = "time-core"
-version = "0.1.8"
+version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
+checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca"
 
 [[package]]
 name = "time-macros"
-version = "0.2.26"
+version = "0.2.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4"
+checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd"
 dependencies = [
  "num-conv",
  "time-core",
@@ -9988,9 +10011,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
 
 [[package]]
 name = "wasip2"
-version = "1.0.2+wasi-0.2.9"
+version = "1.0.1+wasi-0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
+checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
 dependencies = [
  "wit-bindgen",
 ]
@@ -10003,9 +10026,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.108"
+version = "0.2.106"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
+checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
 dependencies = [
  "cfg-if",
  "once_cell",
@@ -10016,12 +10039,11 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-futures"
-version = "0.4.58"
+version = "0.4.56"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f"
+checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c"
 dependencies = [
  "cfg-if",
- "futures-util",
  "js-sys",
  "once_cell",
  "wasm-bindgen",
@@ -10030,9 +10052,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.108"
+version = "0.2.106"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
+checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -10040,9 +10062,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.108"
+version = "0.2.106"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
+checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
 dependencies = [
  "bumpalo",
  "proc-macro2",
@@ -10053,9 +10075,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.108"
+version = "0.2.106"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
+checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
 dependencies = [
  "unicode-ident",
 ]
@@ -10089,9 +10111,9 @@ dependencies = [
 
 [[package]]
 name = "web-sys"
-version = "0.3.85"
+version = "0.3.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
+checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -10173,7 +10195,7 @@ version = "0.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
 dependencies = [
- "windows-sys 0.48.0",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -10698,9 +10720,9 @@ dependencies = [
 
 [[package]]
 name = "wit-bindgen"
-version = "0.51.0"
+version = "0.46.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
 
 [[package]]
 name = "writeable"
@@ -10855,18 +10877,18 @@ dependencies = [
 
 [[package]]
 name = "zerocopy"
-version = "0.8.34"
+version = "0.8.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d"
+checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
 dependencies = [
  "zerocopy-derive",
 ]
 
 [[package]]
 name = "zerocopy-derive"
-version = "0.8.34"
+version = "0.8.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d"
+checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -10969,9 +10991,9 @@ checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3"
 
 [[package]]
 name = "zmij"
-version = "1.0.17"
+version = "1.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
+checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea"
 
 [[package]]
 name = "zopfli"
diff --git a/Cargo.toml b/Cargo.toml
index 129bc2203..e2aba3ca2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -29,6 +29,8 @@ members = [
     "core/cli",
     "core/clock",
     "core/common",
+    "core/configs",
+    "core/configs_derive",
     "core/connectors/runtime",
     "core/connectors/sdk",
     "core/connectors/sinks/elasticsearch_sink",
@@ -110,10 +112,13 @@ compio-tls = { version = "0.8.0", features = [
     "rustls",
 ], default-features = false }
 compio-ws = "0.2.0"
+configs = { path = "core/configs", version = "0.1.0" }
+configs_derive = { path = "core/configs_derive", version = "0.1.0" }
 console-subscriber = "0.5.0"
 crossbeam = "0.8.4"
 cyper = { version = "0.7.1", features = ["rustls"], default-features = false }
 cyper-axum = { version = "0.7.1" }
+darling = "0.20"
 dashmap = "6.1.0"
 derive-new = "0.7.0"
 derive_builder = "0.20.2"
@@ -173,7 +178,9 @@ parquet = "57.2.0"
 passterm = "=2.0.1"
 postcard = { version = "1.1.3", features = ["alloc"] }
 predicates = "3.1.3"
+proc-macro2 = "1"
 quinn = "0.11.9"
+quote = "1"
 rand = "0.9.2"
 rand_xoshiro = "0.7.0"
 regex = "1.12.2"
@@ -200,6 +207,7 @@ simd-json = { version = "0.17.0", features = ["serde_impl"] }
 slab = "0.4.11"
 strum = { version = "0.27.2", features = ["derive"] }
 strum_macros = "0.27.2"
+syn = { version = "2", features = ["full", "extra-traits"] }
 sysinfo = "0.38.0"
 tempfile = "3.24.0"
 test-case = "3.3.1"
diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md
index 1320800e7..c00b459ed 100644
--- a/DEPENDENCIES.md
+++ b/DEPENDENCIES.md
@@ -71,8 +71,8 @@ atomic-polyfill: 1.0.3, "Apache-2.0 OR MIT",
 atomic-waker: 1.1.2, "Apache-2.0 OR MIT",
 autocfg: 1.5.0, "Apache-2.0 OR MIT",
 autotools: 0.2.7, "MIT",
-aws-lc-rs: 1.15.4, "(Apache-2.0 OR ISC) AND ISC",
-aws-lc-sys: 0.37.0, "(Apache-2.0 OR ISC) AND ISC AND OpenSSL",
+aws-lc-rs: 1.15.2, "(Apache-2.0 OR ISC) AND ISC",
+aws-lc-sys: 0.35.0, "(Apache-2.0 OR ISC) AND ISC AND OpenSSL",
 axum: 0.8.8, "MIT",
 axum-core: 0.5.6, "MIT",
 axum-macros: 0.5.0, "MIT",
@@ -123,7 +123,7 @@ bzip2: 0.6.1, "Apache-2.0 OR MIT",
 camino: 1.2.2, "Apache-2.0 OR MIT",
 cargo-platform: 0.3.2, "Apache-2.0 OR MIT",
 cargo_metadata: 0.23.1, "MIT",
-cc: 1.2.54, "Apache-2.0 OR MIT",
+cc: 1.2.52, "Apache-2.0 OR MIT",
 cesu8: 1.1.0, "Apache-2.0 OR MIT",
 cfg-if: 1.0.4, "Apache-2.0 OR MIT",
 cfg_aliases: 0.2.1, "MIT",
@@ -158,6 +158,8 @@ compio-ws: 0.2.0, "MIT",
 compression-codecs: 0.4.36, "Apache-2.0 OR MIT",
 compression-core: 0.4.31, "Apache-2.0 OR MIT",
 concurrent-queue: 2.5.0, "Apache-2.0 OR MIT",
+configs: 0.1.0, "Apache-2.0",
+configs_derive: 0.1.0, "Apache-2.0",
 consensus: 0.1.0, "Apache-2.0",
 console: 0.16.2, "MIT",
 console_error_panic_hook: 0.1.7, "Apache-2.0 OR MIT",
@@ -277,8 +279,8 @@ fiat-crypto: 0.2.9, "Apache-2.0 OR BSD-1-Clause OR MIT",
 figlet-rs: 0.1.5, "Apache-2.0",
 figment: 0.10.19, "Apache-2.0 OR MIT",
 file-operation: 0.8.17, "MIT",
-filetime: 0.2.27, "Apache-2.0 OR MIT",
-find-msvc-tools: 0.1.8, "Apache-2.0 OR MIT",
+filetime: 0.2.26, "Apache-2.0 OR MIT",
+find-msvc-tools: 0.1.7, "Apache-2.0 OR MIT",
 flatbuffers: 25.12.19, "Apache-2.0",
 flate2: 1.1.8, "Apache-2.0 OR MIT",
 float-cmp: 0.10.0, "MIT",
@@ -431,7 +433,7 @@ jni: 0.21.1, "Apache-2.0 OR MIT",
 jni-sys: 0.3.0, "Apache-2.0 OR MIT",
 jobserver: 0.1.34, "Apache-2.0 OR MIT",
 journal: 0.1.0, "Apache-2.0",
-js-sys: 0.3.85, "Apache-2.0 OR MIT",
+js-sys: 0.3.83, "Apache-2.0 OR MIT",
 jsonwebtoken: 10.2.0, "MIT",
 jwalk: 0.8.1, "MIT",
 keccak: 0.1.5, "Apache-2.0 OR MIT",
@@ -454,8 +456,8 @@ libc: 0.2.180, "Apache-2.0 OR MIT",
 libdbus-sys: 0.2.7, "Apache-2.0 OR MIT",
 libgit2-sys: 0.18.3+1.9.2, "Apache-2.0 OR MIT",
 liblzma: 0.4.5, "Apache-2.0 OR MIT",
-liblzma-sys: 0.4.5, "Apache-2.0 OR MIT",
-libm: 0.2.16, "MIT",
+liblzma-sys: 0.4.4, "Apache-2.0 OR MIT",
+libm: 0.2.15, "MIT",
 libmimalloc-sys: 0.1.44, "MIT",
 libredox: 0.1.12, "MIT",
 libsqlite3-sys: 0.30.1, "MIT",
@@ -504,7 +506,7 @@ nonzero_ext: 0.3.0, "Apache-2.0",
 nonzero_lit: 0.1.2, "Apache-2.0 OR CC0-1.0 OR MIT",
 normalize-line-endings: 0.3.0, "Apache-2.0",
 notify: 8.2.0, "CC0-1.0",
-notify-types: 2.1.0, "Apache-2.0 OR MIT",
+notify-types: 2.0.0, "Apache-2.0 OR MIT",
 nougat: 0.2.4, "Apache-2.0 OR MIT OR Zlib",
 nougat-proc_macros: 0.2.4, "Apache-2.0 OR MIT OR Zlib",
 ntapi: 0.4.2, "Apache-2.0 OR MIT",
@@ -513,7 +515,7 @@ num: 0.4.3, "Apache-2.0 OR MIT",
 num-bigint: 0.4.6, "Apache-2.0 OR MIT",
 num-bigint-dig: 0.8.6, "Apache-2.0 OR MIT",
 num-complex: 0.4.6, "Apache-2.0 OR MIT",
-num-conv: 0.2.0, "Apache-2.0 OR MIT",
+num-conv: 0.1.0, "Apache-2.0 OR MIT",
 num-integer: 0.1.46, "Apache-2.0 OR MIT",
 num-iter: 0.1.45, "Apache-2.0 OR MIT",
 num-modular: 0.6.1, "Apache-2.0",
@@ -534,7 +536,7 @@ opaque-debug: 0.3.1, "Apache-2.0 OR MIT",
 opendal: 0.55.0, "Apache-2.0",
 openssl: 0.10.75, "Apache-2.0",
 openssl-macros: 0.1.1, "Apache-2.0 OR MIT",
-openssl-probe: 0.2.1, "Apache-2.0 OR MIT",
+openssl-probe: 0.2.0, "Apache-2.0 OR MIT",
 openssl-src: 300.5.4+3.5.4, "Apache-2.0 OR MIT",
 openssl-sys: 0.9.111, "MIT",
 opentelemetry: 0.31.0, "Apache-2.0",
@@ -601,7 +603,7 @@ proc-macro-crate: 1.3.1, "Apache-2.0 OR MIT",
 proc-macro-crate: 3.4.0, "Apache-2.0 OR MIT",
 proc-macro-error: 1.0.4, "Apache-2.0 OR MIT",
 proc-macro-error-attr: 1.0.4, "Apache-2.0 OR MIT",
-proc-macro2: 1.0.106, "Apache-2.0 OR MIT",
+proc-macro2: 1.0.105, "Apache-2.0 OR MIT",
 proc-macro2-diagnostics: 0.10.1, "Apache-2.0 OR MIT",
 prometheus-client: 0.24.0, "Apache-2.0 OR MIT",
 prometheus-client-derive-encode: 0.5.0, "Apache-2.0 OR MIT",
@@ -620,7 +622,7 @@ quick-xml: 0.38.4, "MIT",
 quinn: 0.11.9, "Apache-2.0 OR MIT",
 quinn-proto: 0.11.13, "Apache-2.0 OR MIT",
 quinn-udp: 0.5.14, "Apache-2.0 OR MIT",
-quote: 1.0.44, "Apache-2.0 OR MIT",
+quote: 1.0.43, "Apache-2.0 OR MIT",
 r-efi: 5.3.0, "Apache-2.0 OR LGPL-2.1-or-later OR MIT",
 radium: 0.7.0, "MIT",
 rand: 0.8.5, "Apache-2.0 OR MIT",
@@ -674,10 +676,10 @@ rustix: 1.1.3, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
 rustls: 0.23.36, "Apache-2.0 OR ISC OR MIT",
 rustls-native-certs: 0.8.3, "Apache-2.0 OR ISC OR MIT",
 rustls-pemfile: 2.2.0, "Apache-2.0 OR ISC OR MIT",
-rustls-pki-types: 1.14.0, "Apache-2.0 OR MIT",
+rustls-pki-types: 1.13.2, "Apache-2.0 OR MIT",
 rustls-platform-verifier: 0.6.2, "Apache-2.0 OR MIT",
 rustls-platform-verifier-android: 0.1.1, "Apache-2.0 OR MIT",
-rustls-webpki: 0.103.9, "ISC",
+rustls-webpki: 0.103.8, "ISC",
 rustversion: 1.0.22, "Apache-2.0 OR MIT",
 ryu: 1.0.22, "Apache-2.0 OR BSL-1.0",
 same-file: 1.0.6, "MIT OR Unlicense",
@@ -785,9 +787,9 @@ thiserror-impl: 1.0.69, "Apache-2.0 OR MIT",
 thiserror-impl: 2.0.18, "Apache-2.0 OR MIT",
 thread_local: 1.1.9, "Apache-2.0 OR MIT",
 thrift: 0.17.0, "Apache-2.0",
-time: 0.3.46, "Apache-2.0 OR MIT",
-time-core: 0.1.8, "Apache-2.0 OR MIT",
-time-macros: 0.2.26, "Apache-2.0 OR MIT",
+time: 0.3.45, "Apache-2.0 OR MIT",
+time-core: 0.1.7, "Apache-2.0 OR MIT",
+time-macros: 0.2.25, "Apache-2.0 OR MIT",
 tiny-keccak: 2.0.2, "CC0-1.0",
 tinystr: 0.8.2, "Unicode-3.0",
 tinyvec: 1.10.0, "Apache-2.0 OR MIT OR Zlib",
@@ -871,16 +873,16 @@ wait-timeout: 0.2.1, "Apache-2.0 OR MIT",
 walkdir: 2.5.0, "MIT OR Unlicense",
 want: 0.3.1, "MIT",
 wasi: 0.11.1+wasi-snapshot-preview1, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
-wasip2: 1.0.2+wasi-0.2.9, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
+wasip2: 1.0.1+wasi-0.2.4, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
 wasite: 0.1.0, "Apache-2.0 OR BSL-1.0 OR MIT",
-wasm-bindgen: 0.2.108, "Apache-2.0 OR MIT",
-wasm-bindgen-futures: 0.4.58, "Apache-2.0 OR MIT",
-wasm-bindgen-macro: 0.2.108, "Apache-2.0 OR MIT",
-wasm-bindgen-macro-support: 0.2.108, "Apache-2.0 OR MIT",
-wasm-bindgen-shared: 0.2.108, "Apache-2.0 OR MIT",
+wasm-bindgen: 0.2.106, "Apache-2.0 OR MIT",
+wasm-bindgen-futures: 0.4.56, "Apache-2.0 OR MIT",
+wasm-bindgen-macro: 0.2.106, "Apache-2.0 OR MIT",
+wasm-bindgen-macro-support: 0.2.106, "Apache-2.0 OR MIT",
+wasm-bindgen-shared: 0.2.106, "Apache-2.0 OR MIT",
 wasm-streams: 0.4.2, "Apache-2.0 OR MIT",
 wasmtimer: 0.4.3, "MIT",
-web-sys: 0.3.85, "Apache-2.0 OR MIT",
+web-sys: 0.3.83, "Apache-2.0 OR MIT",
 web-time: 1.1.0, "Apache-2.0 OR MIT",
 webpki-root-certs: 1.0.5, "CDLA-Permissive-2.0",
 webpki-roots: 0.26.11, "CDLA-Permissive-2.0",
@@ -953,7 +955,7 @@ windows_x86_64_msvc: 0.52.6, "Apache-2.0 OR MIT",
 windows_x86_64_msvc: 0.53.1, "Apache-2.0 OR MIT",
 winnow: 0.5.40, "MIT",
 winnow: 0.7.14, "MIT",
-wit-bindgen: 0.51.0, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
+wit-bindgen: 0.46.0, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
 writeable: 0.6.2, "Unicode-3.0",
 wyz: 0.5.1, "MIT",
 x509-parser: 0.18.0, "Apache-2.0 OR MIT",
@@ -966,8 +968,8 @@ yew-router: 0.19.0, "Apache-2.0 OR MIT",
 yew-router-macro: 0.19.0, "Apache-2.0 OR MIT",
 yoke: 0.8.1, "Unicode-3.0",
 yoke-derive: 0.8.1, "Unicode-3.0",
-zerocopy: 0.8.34, "Apache-2.0 OR BSD-2-Clause OR MIT",
-zerocopy-derive: 0.8.34, "Apache-2.0 OR BSD-2-Clause OR MIT",
+zerocopy: 0.8.33, "Apache-2.0 OR BSD-2-Clause OR MIT",
+zerocopy-derive: 0.8.33, "Apache-2.0 OR BSD-2-Clause OR MIT",
 zerofrom: 0.1.6, "Unicode-3.0",
 zerofrom-derive: 0.1.6, "Unicode-3.0",
 zeroize: 1.8.2, "Apache-2.0 OR MIT",
@@ -977,7 +979,7 @@ zerovec: 0.11.5, "Unicode-3.0",
 zerovec-derive: 0.11.2, "Unicode-3.0",
 zip: 7.2.0, "MIT",
 zlib-rs: 0.5.5, "Zlib",
-zmij: 1.0.17, "MIT",
+zmij: 1.0.14, "MIT",
 zopfli: 0.8.3, "Apache-2.0",
 zstd: 0.13.3, "MIT",
 zstd-safe: 7.2.4, "Apache-2.0 OR MIT",
diff --git a/README.md b/README.md
index 3718f2931..a847968e7 100644
--- a/README.md
+++ b/README.md
@@ -211,11 +211,11 @@ ulimits:
 
 ## Configuration
 
-The default configuration can be found in `server.toml` file in `configs` directory.
+The default configuration can be found in `config.toml` file in `core/server` directory.
 
-The configuration file is loaded from the current working directory, but you can specify the path to the configuration file by setting `IGGY_CONFIG_PATH` environment variable, for example `export IGGY_CONFIG_PATH=configs/server.toml` (or other command depending on OS).
+The configuration file is loaded from the current working directory, but you can specify the path to the configuration file by setting `IGGY_CONFIG_PATH` environment variable, for example `export IGGY_CONFIG_PATH=core/server/config.toml` (or other command depending on OS).
 
-When config file is not found, the default values from embedded `server.toml` file are used.
+When config file is not found, the default values from embedded `config.toml` file are used.
 
 For the detailed documentation of the configuration file, please refer to the [configuration](https://iggy.apache.org/docs/server/configuration) section.
 
@@ -257,7 +257,7 @@ Start the server:
 
 `cargo run --bin iggy-server`
 
-All the data used by the server will be persisted under the `local_data` directory by default, unless specified differently in the configuration (see `system.path` in `server.toml`).
+All the data used by the server will be persisted under the `local_data` directory by default, unless specified differently in the configuration (see `system.path` in `config.toml`).
 
 One can use default root credentials with optional `--with-default-root-credentials`.
 This flag is equivalent to setting `IGGY_ROOT_USERNAME=iggy` and `IGGY_ROOT_PASSWORD=iggy`, plus
diff --git a/bdd/docker-compose.yml b/bdd/docker-compose.yml
index 495fbc080..1062bb780 100644
--- a/bdd/docker-compose.yml
+++ b/bdd/docker-compose.yml
@@ -28,7 +28,7 @@ services:
         PREBUILT_IGGY_CLI: ${IGGY_CLI_PATH:-target/debug/iggy}
         LIBC: glibc
         PROFILE: debug
-    command: ["--fresh", "--with-default-root-credentials"]
+    command: [ "--fresh", "--with-default-root-credentials" ]
     cap_add:
       - SYS_NICE
     security_opt:
@@ -38,7 +38,7 @@ services:
         soft: -1
         hard: -1
     healthcheck:
-      test: ["CMD", "/usr/local/bin/iggy", "--tcp-server-address", "127.0.0.1:8090", "ping"]
+      test: [ "CMD", "/usr/local/bin/iggy", "--tcp-server-address", "127.0.0.1:8090", "ping" ]
       interval: 20s
       timeout: 3s
       retries: 30
@@ -68,7 +68,7 @@ services:
         LIBC: glibc
         PROFILE: debug
     # Leader runs without --follower flag
-    command: ["--fresh", "--with-default-root-credentials"]
+    command: [ "--fresh", "--with-default-root-credentials" ]
     cap_add:
       - SYS_NICE
     security_opt:
@@ -78,7 +78,7 @@ services:
         soft: -1
         hard: -1
     healthcheck:
-      test: ["CMD", "/usr/local/bin/iggy", "--tcp-server-address", "127.0.0.1:8091", "ping"]
+      test: [ "CMD", "/usr/local/bin/iggy", "--tcp-server-address", "127.0.0.1:8091", "ping" ]
       interval: 20s
       timeout: 3s
       retries: 30
@@ -94,7 +94,6 @@ services:
       # Cluster configuration
       - IGGY_CLUSTER_ENABLED=true
       - IGGY_CLUSTER_NAME=test-cluster
-      - IGGY_CLUSTER_ID=1
       # Current node identity
       - IGGY_CLUSTER_NODE_CURRENT_NAME=leader-node
       - IGGY_CLUSTER_NODE_CURRENT_IP=iggy-leader
@@ -123,7 +122,7 @@ services:
         LIBC: glibc
         PROFILE: debug
     # Follower runs with --follower flag
-    command: ["--fresh", "--with-default-root-credentials", "--follower"]
+    command: [ "--fresh", "--with-default-root-credentials", "--follower" ]
     cap_add:
       - SYS_NICE
     security_opt:
@@ -133,7 +132,7 @@ services:
         soft: -1
         hard: -1
     healthcheck:
-      test: ["CMD", "/usr/local/bin/iggy", "--tcp-server-address", "127.0.0.1:8092", "ping"]
+      test: [ "CMD", "/usr/local/bin/iggy", "--tcp-server-address", "127.0.0.1:8092", "ping" ]
       interval: 20s
       timeout: 3s
       retries: 30
@@ -149,7 +148,6 @@ services:
       # Cluster configuration (same as leader)
       - IGGY_CLUSTER_ENABLED=true
       - IGGY_CLUSTER_NAME=test-cluster
-      - IGGY_CLUSTER_ID=1
       # Current node identity (different from leader)
       - IGGY_CLUSTER_NODE_CURRENT_NAME=follower-node
       - IGGY_CLUSTER_NODE_CURRENT_IP=iggy-follower
@@ -188,15 +186,7 @@ services:
     volumes:
       - ./scenarios/basic_messaging.feature:/app/features/basic_messaging.feature
       - ./scenarios/leader_redirection.feature:/app/features/leader_redirection.feature
-    command:
-      [
-        "cargo",
-        "test",
-        "-p",
-        "bdd",
-        "--features",
-        "bdd",
-      ]
+    command: [ "cargo", "test", "-p", "bdd", "--features", "bdd" ]
     networks:
       - iggy-bdd-network
 
@@ -213,7 +203,7 @@ services:
     volumes:
       - ./scenarios/basic_messaging.feature:/app/features/basic_messaging.feature
     working_dir: /app
-    command: ["pytest", "tests/", "-v"]
+    command: [ "pytest", "tests/", "-v" ]
     networks:
       - iggy-bdd-network
 
@@ -229,7 +219,7 @@ services:
       - IGGY_TCP_ADDRESS=iggy-server:8090
     volumes:
       - ./scenarios/basic_messaging.feature:/app/features/basic_messaging.feature
-    command: ["go", "test", "-v", "./..."]
+    command: [ "go", "test", "-v", "./..." ]
     networks:
       - iggy-bdd-network
 
@@ -244,7 +234,7 @@ services:
       - IGGY_ROOT_USERNAME=iggy
       - IGGY_ROOT_PASSWORD=iggy
       - IGGY_TCP_ADDRESS=iggy-server:8090
-    command: ["npm", "run", "test:bdd"]
+    command: [ "npm", "run", "test:bdd" ]
     networks:
       - iggy-bdd-network
 
@@ -258,7 +248,7 @@ services:
       - IGGY_ROOT_USERNAME=iggy
       - IGGY_ROOT_PASSWORD=iggy
       - IGGY_TCP_ADDRESS=iggy-server:8090
-    command: ["dotnet", "test"]
+    command: [ "dotnet", "test" ]
     networks:
       - iggy-bdd-network
 
@@ -275,7 +265,7 @@ services:
       - IGGY_TCP_ADDRESS=iggy-server:8090
     volumes:
       - ./scenarios/basic_messaging.feature:/app/features/basic_messaging.feature
-    command: ["gradle", "--no-daemon", "test"]
+    command: [ "gradle", "--no-daemon", "test" ]
     networks:
       - iggy-bdd-network
 
diff --git a/core/ai/mcp/Cargo.toml b/core/ai/mcp/Cargo.toml
index d9c8b9ad9..8cb193aa7 100644
--- a/core/ai/mcp/Cargo.toml
+++ b/core/ai/mcp/Cargo.toml
@@ -29,12 +29,13 @@ readme = "README.md"
 [dependencies]
 axum = { workspace = true }
 axum-server = { workspace = true }
+configs = { workspace = true }
+configs_derive = { workspace = true }
 dirs = { workspace = true }
 dotenvy = { workspace = true }
 figlet-rs = { workspace = true }
 figment = { workspace = true }
 iggy = { workspace = true }
-iggy_common = { workspace = true }
 opentelemetry = { workspace = true }
 opentelemetry-appender-tracing = { workspace = true }
 opentelemetry-otlp = { workspace = true }
diff --git a/core/ai/mcp/src/configs.rs b/core/ai/mcp/src/configs.rs
index 7ec2846c3..f745ef60c 100644
--- a/core/ai/mcp/src/configs.rs
+++ b/core/ai/mcp/src/configs.rs
@@ -17,48 +17,53 @@
  * under the License.
  */
 
+use ::configs::{FileConfigProvider, TypedEnvProvider};
 use axum::http::Method;
+use configs_derive::ConfigEnv;
 use figment::{
     Metadata, Profile, Provider,
     providers::{Format, Toml},
     value::Dict,
 };
 use iggy::prelude::{DEFAULT_ROOT_PASSWORD, DEFAULT_ROOT_USERNAME};
-use iggy_common::{CustomEnvProvider, FileConfigProvider};
 use serde::{Deserialize, Serialize};
 use std::fmt::{Display as FmtDisplay, Formatter};
 use std::str::FromStr;
 use strum::Display;
 use tower_http::cors::{AllowOrigin, CorsLayer};
 
-#[derive(Debug, Clone, Deserialize, Serialize)]
+#[derive(Debug, Clone, Deserialize, Serialize, ConfigEnv)]
+#[config_env(prefix = "IGGY_MCP_", name = "iggy-mcp-server-config")]
 #[serde(default)]
 pub struct McpServerConfig {
     pub http: HttpConfig,
     pub iggy: IggyConfig,
     pub permissions: PermissionsConfig,
+    #[config_env(leaf)]
     pub transport: McpTransport,
     pub telemetry: TelemetryConfig,
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, ConfigEnv)]
 pub struct IggyConfig {
     pub address: String,
     pub username: String,
+    #[config_env(secret)]
     pub password: String,
+    #[config_env(secret)]
     pub token: String,
     pub consumer: String,
     pub tls: IggyTlsConfig,
 }
 
-#[derive(Debug, Default, Clone, Serialize, Deserialize)]
+#[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigEnv)]
 pub struct IggyTlsConfig {
     pub enabled: bool,
     pub ca_file: String,
     pub domain: Option<String>,
 }
 
-#[derive(Debug, Clone, Deserialize, Serialize)]
+#[derive(Debug, Clone, Deserialize, Serialize, ConfigEnv)]
 pub struct PermissionsConfig {
     pub create: bool,
     pub read: bool,
@@ -66,7 +71,7 @@ pub struct PermissionsConfig {
     pub delete: bool,
 }
 
-#[derive(Debug, Clone, Deserialize, Serialize)]
+#[derive(Debug, Clone, Deserialize, Serialize, ConfigEnv)]
 pub struct HttpConfig {
     pub address: String,
     pub path: String,
@@ -74,14 +79,14 @@ pub struct HttpConfig {
     pub tls: HttpTlsConfig,
 }
 
-#[derive(Debug, Default, Deserialize, Serialize, Clone)]
+#[derive(Debug, Default, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct HttpTlsConfig {
     pub enabled: bool,
     pub cert_file: String,
     pub key_file: String,
 }
 
-#[derive(Debug, Default, Deserialize, Serialize, Clone)]
+#[derive(Debug, Default, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct HttpCorsConfig {
     pub enabled: bool,
     pub allowed_methods: Vec<String>,
@@ -103,7 +108,7 @@ pub enum McpTransport {
     Stdio,
 }
 
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct TelemetryConfig {
     pub enabled: bool,
     pub service_name: String,
@@ -132,8 +137,9 @@ impl FmtDisplay for TelemetryConfig {
     }
 }
 
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct TelemetryLogsConfig {
+    #[config_env(leaf)]
     pub transport: TelemetryTransport,
     pub endpoint: String,
 }
@@ -157,8 +163,9 @@ impl FmtDisplay for TelemetryLogsConfig {
     }
 }
 
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct TelemetryTracesConfig {
+    #[config_env(leaf)]
     pub transport: TelemetryTransport,
     pub endpoint: String,
 }
@@ -396,25 +403,23 @@ impl McpServerConfig {
 
 #[derive(Debug, Clone)]
 pub struct McpServerEnvProvider {
-    provider: CustomEnvProvider<McpServerConfig>,
+    provider: TypedEnvProvider<McpServerConfig>,
 }
 
 impl Default for McpServerEnvProvider {
     fn default() -> Self {
         Self {
-            provider: CustomEnvProvider::new("IGGY_MCP_", &[]),
+            provider: TypedEnvProvider::from_config(McpServerConfig::ENV_PREFIX),
         }
     }
 }
 
 impl Provider for McpServerEnvProvider {
     fn metadata(&self) -> Metadata {
-        Metadata::named("iggy-mcp-server-config")
+        Metadata::named(McpServerConfig::ENV_PROVIDER_NAME)
     }
 
     fn data(&self) -> Result<figment::value::Map<Profile, Dict>, figment::Error> {
-        self.provider.deserialize().map_err(|_| {
-            figment::Error::from("Cannot deserialize environment variables for MCP config")
-        })
+        self.provider.data()
     }
 }
diff --git a/core/ai/mcp/src/main.rs b/core/ai/mcp/src/main.rs
index dbbd1bf4c..73135cf1c 100644
--- a/core/ai/mcp/src/main.rs
+++ b/core/ai/mcp/src/main.rs
@@ -17,12 +17,12 @@
  * under the License.
  */
 
+use ::configs::ConfigProvider;
 use configs::{McpServerConfig, McpTransport};
 use dotenvy::dotenv;
 use error::McpRuntimeError;
 use figlet_rs::FIGfont;
 use iggy::prelude::{Client, Identifier};
-use iggy_common::ConfigProvider;
 use rmcp::{ServiceExt, model::ErrorData, transport::stdio};
 use service::IggyService;
 use std::{env, sync::Arc};
diff --git a/core/common/Cargo.toml b/core/common/Cargo.toml
index 52ea4c5ea..fbcb02cd7 100644
--- a/core/common/Cargo.toml
+++ b/core/common/Cargo.toml
@@ -48,11 +48,11 @@ compio = { workspace = true }
 compio-quic = { workspace = true }
 compio-tls = { workspace = true }
 compio-ws = { workspace = true }
+configs_derive = { workspace = true }
 crossbeam = { workspace = true }
 derive_more = { workspace = true }
 err_trail = { workspace = true }
 fast-async-mutex = { version = "0.6.7", optional = true }
-figment = { workspace = true }
 human-repr = { workspace = true }
 humantime = { workspace = true }
 nix = { workspace = true }
@@ -66,7 +66,6 @@ serde_with = { workspace = true, features = ["base64"] }
 strum = { workspace = true }
 thiserror = { workspace = true }
 tokio = { workspace = true }
-toml = { workspace = true }
 tracing = { workspace = true }
 tungstenite = { workspace = true }
 twox-hash = { workspace = true }
diff --git a/core/common/src/configs/mod.rs b/core/common/src/configs/mod.rs
deleted file mode 100644
index fc440820f..000000000
--- a/core/common/src/configs/mod.rs
+++ /dev/null
@@ -1,893 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-//! Configuration module providing flexible configuration loading with file and environment support.
-//!
-//! This module provides a trait-based configuration system that supports:
-//! - Loading configuration from TOML files
-//! - Environment variable overrides with complex path resolution
-//! - Array and HashMap field overrides
-//! - JSON value field handling
-//! - Automatic type conversion and validation
-
-use figment::{
-    Figment, Profile, Provider,
-    providers::{Data, Format, Toml},
-    value::{Dict, Map as FigmentMap, Tag, Value as FigmentValue},
-};
-use serde::{Serialize, de::DeserializeOwned};
-use std::{env, fmt::Display, future::Future, marker::PhantomData, path::Path};
-use toml::{Value as TomlValue, map::Map as TomlMap};
-use tracing::{error, info, warn};
-
-const SECRET_MASK: &str = "******";
-const ARRAY_SEPARATOR: char = '_';
-const PATH_SEPARATOR: &str = ".";
-const DISPLAY_CONFIG_ENV: &str = "IGGY_DISPLAY_CONFIG";
-
-type ProfileMap = FigmentMap<Profile, Dict>;
-
-/// Type alias for configuration that can be serialized, deserialized, has defaults, and can be displayed
-pub trait ConfigurationType: Serialize + DeserializeOwned + Default + Display {}
-impl<T: Serialize + DeserializeOwned + Default + Display> ConfigurationType for T {}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum ConfigurationError {
-    CannotLoadConfiguration,
-    DefaultSerializationFailed,
-    DefaultParsingFailed,
-    EnvironmentVariableParsingFailed,
-    InvalidConfigurationValue,
-}
-
-impl Display for ConfigurationError {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            Self::CannotLoadConfiguration => write!(f, "Cannot load configuration"),
-            Self::DefaultSerializationFailed => {
-                write!(f, "Failed to serialize default configuration")
-            }
-            Self::DefaultParsingFailed => write!(f, "Failed to parse default configuration"),
-            Self::EnvironmentVariableParsingFailed => {
-                write!(f, "Failed to parse environment variables")
-            }
-            Self::InvalidConfigurationValue => {
-                write!(f, "Provided configuration value is invalid")
-            }
-        }
-    }
-}
-
-impl std::error::Error for ConfigurationError {}
-
-pub trait ConfigProvider<T: ConfigurationType> {
-    fn load_config(&self) -> impl Future<Output = Result<T, ConfigurationError>>;
-}
-
-/// File-based configuration provider that combines file, default, and environment configurations
-pub struct FileConfigProvider<P> {
-    file_path: String,
-    default_config: Option<Data<Toml>>,
-    env_provider: P,
-    display_config: bool,
-}
-
-impl<P: Provider> FileConfigProvider<P> {
-    /// Create a new file configuration provider
-    ///
-    /// # Arguments
-    /// * `file_path` - Path to the configuration file
-    /// * `env_provider` - Environment variable provider
-    /// * `display_config` - Whether to display the loaded configuration
-    /// * `default_config` - Optional default configuration data
-    pub fn new(
-        file_path: String,
-        env_provider: P,
-        display_config: bool,
-        default_config: Option<Data<Toml>>,
-    ) -> Self {
-        Self {
-            file_path,
-            env_provider,
-            default_config,
-            display_config,
-        }
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct CustomEnvProvider<T: ConfigurationType> {
-    prefix: String,
-    secret_keys: Vec<String>,
-    _phantom: PhantomData<T>,
-}
-
-impl<T: ConfigurationType> CustomEnvProvider<T> {
-    /// Create a new custom environment provider
-    ///
-    /// # Arguments
-    /// * `prefix` - Environment variable prefix to filter by
-    /// * `secret_keys` - Keys that should be masked in logs
-    pub fn new(prefix: &str, secret_keys: &[&str]) -> Self {
-        Self {
-            prefix: prefix.to_string(),
-            secret_keys: secret_keys.iter().map(|s| s.to_string()).collect(),
-            _phantom: PhantomData,
-        }
-    }
-
-    /// Deserialize environment variables into a configuration profile map
-    pub fn deserialize(&self) -> Result<ProfileMap, ConfigurationError> {
-        let default_config = toml::to_string(&T::default())
-            .map_err(|_| ConfigurationError::DefaultSerializationFailed)?;
-
-        let toml_value: TomlValue = toml::from_str(&default_config)
-            .map_err(|_| ConfigurationError::DefaultParsingFailed)?;
-
-        let mut source_dict = Dict::new();
-        if let TomlValue::Table(table) = toml_value {
-            Self::walk_toml_table_to_dict("", table, &mut source_dict);
-        }
-
-        let mut target_dict = Dict::new();
-        for (key, mut value) in env::vars() {
-            let env_key = key.to_uppercase();
-            if !env_key.starts_with(&self.prefix) {
-                continue;
-            }
-
-            let keys: Vec<String> = env_key[self.prefix.len()..]
-                .split(ARRAY_SEPARATOR)
-                .map(|k| k.to_lowercase())
-                .collect();
-
-            let expected_type = Self::find_expected_type(&source_dict, &keys);
-            let env_var_value = Self::parse_environment_value_typed(&value, expected_type);
-
-            if self.secret_keys.contains(&env_key) {
-                value = SECRET_MASK.to_string();
-            }
-
-            info!("{env_key} value changed to: {value} from environment variable");
-            Self::insert_environment_override(&source_dict, &mut target_dict, keys, env_var_value);
-        }
-
-        let mut data = ProfileMap::new();
-        data.insert(Profile::default(), target_dict);
-        Ok(data)
-    }
-
-    /// Walk through TOML table and convert to dictionary structure
-    fn walk_toml_table_to_dict(prefix: &str, table: TomlMap<String, TomlValue>, dict: &mut Dict) {
-        for (key, value) in table {
-            let new_prefix = if prefix.is_empty() {
-                key.clone()
-            } else {
-                format!("{prefix}{PATH_SEPARATOR}{key}")
-            };
-
-            match value {
-                TomlValue::Table(inner_table) => {
-                    let mut nested_dict = Dict::new();
-                    Self::walk_toml_table_to_dict(&new_prefix, inner_table, &mut nested_dict);
-                    dict.insert(key, FigmentValue::from(nested_dict));
-                }
-                _ => {
-                    dict.insert(key, Self::toml_to_figment_value(&value));
-                }
-            }
-        }
-    }
-
-    /// Insert environment variable override into target dictionary
-    fn insert_environment_override(
-        source: &Dict,
-        target: &mut Dict,
-        keys: Vec<String>,
-        value: FigmentValue,
-    ) {
-        if keys.is_empty() {
-            return;
-        }
-
-        // Detect array patterns (e.g., cluster.nodes.0.id)
-        if let Some((idx_pos, array_index)) = Self::find_array_index(&keys) {
-            Self::handle_array_override(source, target, &keys, idx_pos, array_index, value);
-        } else {
-            Self::handle_dictionary_override(source, target, &keys, value);
-        }
-    }
-
-    /// Find array index in key path
-    fn find_array_index(keys: &[String]) -> Option<(usize, usize)> {
-        keys.iter()
-            .enumerate()
-            .find_map(|(i, key)| key.parse::<usize>().ok().map(|idx| (i, idx)))
-    }
-
-    /// Handle array field override with complex path resolution
-    fn handle_array_override(
-        source: &Dict,
-        target: &mut Dict,
-        keys: &[String],
-        idx_pos: usize,
-        array_index: usize,
-        value: FigmentValue,
-    ) {
-        let path_to_array = &keys[..idx_pos];
-        let remaining_path = &keys[idx_pos + 1..];
-
-        // Navigate to array container
-        let current_target = match Self::navigate_to_dictionary(
-            target,
-            &path_to_array[..path_to_array.len().saturating_sub(1)],
-        ) {
-            Some(dict) => dict,
-            None => return,
-        };
-
-        let array_key = match path_to_array.last() {
-            Some(key) => key.clone(),
-            None => return,
-        };
-
-        // Copy existing array from source if needed
-        if !current_target.contains_key(&array_key) {
-            if let Some(existing_array) = Self::find_source_array(source, path_to_array, &array_key)
-            {
-                current_target.insert(array_key.clone(), existing_array);
-            } else {
-                current_target.insert(
-                    array_key.clone(),
-                    FigmentValue::Array(Tag::Default, Vec::new()),
-                );
-            }
-        }
-
-        // Update array element
-        if let Some(FigmentValue::Array(_, arr)) = current_target.get_mut(&array_key) {
-            // Ensure array is large enough
-            while arr.len() <= array_index {
-                arr.push(FigmentValue::Dict(Tag::Default, Dict::new()));
-            }
-
-            if remaining_path.is_empty() {
-                arr[array_index] = value;
-            } else if let FigmentValue::Dict(_, elem_dict) = &mut arr[array_index] {
-                // TODO(hubcio): this is workaround done by Claude because this code is overly
-                // complicated and I don't want to spend time on it.
-                // For nested structures in arrays, check if we need to create intermediate dicts
-                // Handle the "ports" case where it should be a nested structure
-                if remaining_path.len() >= 2 && remaining_path[0] == "ports" {
-                    // Create the ports dict if it doesn't exist
-                    elem_dict
-                        .entry("ports".to_string())
-                        .or_insert_with(|| FigmentValue::Dict(Tag::Default, Dict::new()));
-
-                    if let Some(FigmentValue::Dict(_, ports_dict)) = elem_dict.get_mut("ports") {
-                        // Insert the specific port value (tcp, quic, http, websocket)
-                        ports_dict.insert(remaining_path[1].clone(), value);
-                    }
-                } else {
-                    // Default behavior for other fields
-                    Self::insert_environment_override(
-                        &Dict::new(),
-                        elem_dict,
-                        remaining_path.to_vec(),
-                        value,
-                    );
-                }
-            }
-        }
-    }
-
-    /// Handle dictionary field override with HashMap detection
-    fn handle_dictionary_override(
-        source: &Dict,
-        target: &mut Dict,
-        keys: &[String],
-        value: FigmentValue,
-    ) {
-        if keys.is_empty() {
-            return;
-        }
-
-        // Try to detect HashMap patterns
-        if let Some(hashmap_result) =
-            Self::try_handle_hashmap_override(source, target, keys, value.clone())
-            && hashmap_result
-        {
-            return;
-        }
-
-        // Fallback to original logic for non-HashMap patterns
-        let mut current_source = source;
-        let mut current_target = target;
-        let mut combined_keys = Vec::new();
-
-        for (i, key) in keys.iter().enumerate() {
-            combined_keys.push(key.clone());
-            let key_to_check = combined_keys.join("_");
-
-            match current_source.get(&key_to_check) {
-                Some(FigmentValue::Dict(_, inner_source_dict)) => {
-                    current_target
-                        .entry(key_to_check.clone())
-                        .or_insert_with(|| FigmentValue::Dict(Tag::Default, Dict::new()));
-
-                    if let Some(FigmentValue::Dict(_, inner_target)) =
-                        current_target.get_mut(&key_to_check)
-                    {
-                        current_source = inner_source_dict;
-                        current_target = inner_target;
-                        combined_keys.clear();
-                    } else {
-                        return;
-                    }
-                }
-                Some(_) => {
-                    current_target.insert(key_to_check, value);
-                    return;
-                }
-                None if i == keys.len() - 1 => {
-                    current_target.insert(key_to_check, value);
-                    return;
-                }
-                _ => continue,
-            }
-        }
-    }
-
-    /// Try to handle HashMap override patterns
-    fn try_handle_hashmap_override(
-        source: &Dict,
-        target: &mut Dict,
-        keys: &[String],
-        value: FigmentValue,
-    ) -> Option<bool> {
-        if keys.len() < 2 {
-            return Some(false);
-        }
-
-        let potential_hashmap_key = &keys[0];
-
-        // Check if this is actually a HashMap field in the source
-        let source_hashmap = match source.get(potential_hashmap_key) {
-            Some(FigmentValue::Dict(_, hashmap_dict)) => {
-                // Determine if this is a HashMap or regular struct
-                let has_dict_values = hashmap_dict
-                    .values()
-                    .any(|v| matches!(v, FigmentValue::Dict(_, _)));
-                let has_non_dict_values = hashmap_dict
-                    .values()
-                    .any(|v| !matches!(v, FigmentValue::Dict(_, _)));
-
-                // If it has non-Dict values mixed with Dict values, it's probably a regular struct
-                if has_non_dict_values && has_dict_values {
-                    return Some(false);
-                }
-
-                // If it only has non-Dict values, it's definitely a regular struct
-                if has_non_dict_values && !has_dict_values {
-                    return Some(false);
-                }
-
-                hashmap_dict
-            }
-            _ => return Some(false),
-        };
-
-        // Try to find the best HashMap entry key
-        if let Some((entry_key, remaining_keys)) =
-            Self::find_optimal_hashmap_split(source_hashmap, &keys[1..])
-        {
-            return Some(Self::apply_hashmap_override(
-                target,
-                potential_hashmap_key,
-                &entry_key,
-                &remaining_keys,
-                source_hashmap,
-                value,
-            ));
-        }
-
-        Some(false)
-    }
-
-    /// Find optimal HashMap key split for environment variable path
-    fn find_optimal_hashmap_split(
-        source_hashmap: &Dict,
-        keys: &[String],
-    ) -> Option<(String, Vec<String>)> {
-        if keys.is_empty() {
-            return None;
-        }
-
-        // First, try existing HashMap entry keys if any exist
-        if !source_hashmap.is_empty() {
-            for (existing_key, existing_value) in source_hashmap {
-                if let FigmentValue::Dict(_, entry_dict) = existing_value {
-                    // Try different ways to match this existing key
-                    for split_point in 1..=keys.len() {
-                        let candidate_key = keys[0..split_point].join("_");
-
-                        if candidate_key == *existing_key {
-                            let remaining_keys = keys[split_point..].to_vec();
-
-                            // Validate that the remaining keys form a valid path in the entry
-                            if remaining_keys.is_empty()
-                                || Self::is_valid_field_path(entry_dict, &remaining_keys)
-                            {
-                                return Some((existing_key.clone(), remaining_keys));
-                            }
-                        }
-                    }
-                }
-            }
-        }
-
-        // For empty HashMaps or when no existing keys match, use simple split
-        let simple_entry_key = keys[0].clone();
-        let simple_remaining = keys[1..].to_vec();
-        Some((simple_entry_key, simple_remaining))
-    }
-
-    /// Check if a field path is valid within a dictionary structure
-    fn is_valid_field_path(dict: &Dict, keys: &[String]) -> bool {
-        if keys.is_empty() {
-            return true;
-        }
-
-        // Try different combinations to account for fields with underscores
-        for split_point in 1..=keys.len() {
-            let field_name = keys[0..split_point].join("_");
-            let remaining_keys = &keys[split_point..];
-
-            if let Some(field_value) = dict.get(&field_name) {
-                if remaining_keys.is_empty() {
-                    return true;
-                } else if let FigmentValue::Dict(_, nested_dict) = field_value {
-                    return Self::is_valid_field_path(nested_dict, remaining_keys);
-                }
-            }
-        }
-
-        // Also try the original approach with combined keys
-        let mut current_dict = dict;
-        let mut combined_keys = Vec::new();
-
-        for key in keys {
-            combined_keys.push(key.clone());
-            let combined_field = combined_keys.join("_");
-
-            if let Some(field_value) = current_dict.get(&combined_field) {
-                match field_value {
-                    FigmentValue::Dict(_, nested_dict) => {
-                        current_dict = nested_dict;
-                        combined_keys.clear();
-                    }
-                    _ => return true,
-                }
-            }
-        }
-
-        true
-    }
-
-    /// Apply HashMap override to target configuration
-    fn apply_hashmap_override(
-        target: &mut Dict,
-        hashmap_key: &str,
-        entry_key: &str,
-        remaining_keys: &[String],
-        source_hashmap: &Dict,
-        value: FigmentValue,
-    ) -> bool {
-        // Ensure the HashMap exists in target
-        target
-            .entry(hashmap_key.to_string())
-            .or_insert_with(|| FigmentValue::Dict(Tag::Default, Dict::new()));
-
-        if let Some(FigmentValue::Dict(_, target_hashmap)) = target.get_mut(hashmap_key) {
-            // Ensure the specific entry exists in the HashMap
-            target_hashmap
-                .entry(entry_key.to_string())
-                .or_insert_with(|| {
-                    if let Some(existing_entry) = source_hashmap.get(entry_key) {
-                        existing_entry.clone()
-                    } else {
-                        FigmentValue::Dict(Tag::Default, Dict::new())
-                    }
-                });
-
-            if remaining_keys.is_empty() {
-                target_hashmap.insert(entry_key.to_string(), value);
-            } else if let Some(FigmentValue::Dict(_, entry_dict)) =
-                target_hashmap.get_mut(entry_key)
-            {
-                // Check if we need special handling for JSON value fields
-                if let Some((json_field, json_keys)) =
-                    Self::find_json_value_field_split(entry_dict, remaining_keys)
-                {
-                    Self::handle_json_value_override(entry_dict, &json_field, json_keys, value);
-                } else {
-                    Self::insert_environment_override(
-                        &Dict::new(),
-                        entry_dict,
-                        remaining_keys.to_vec(),
-                        value,
-                    );
-                }
-            }
-            return true;
-        }
-
-        false
-    }
-
-    /// Find JSON value field split in remaining keys
-    fn find_json_value_field_split(dict: &Dict, keys: &[String]) -> Option<(String, Vec<String>)> {
-        if keys.is_empty() {
-            return None;
-        }
-
-        // Try different split points to find a field that exists
-        for split_point in 1..=keys.len() {
-            let potential_json_field = keys[0..split_point].join("_");
-            let remaining_keys = keys[split_point..].to_vec();
-
-            if dict.contains_key(&potential_json_field) && !remaining_keys.is_empty() {
-                return Some((potential_json_field, remaining_keys));
-            }
-        }
-
-        // If no existing field matches and we have multiple keys, use first key
-        if keys.len() > 1 {
-            let potential_new_field = keys[0].clone();
-            let remaining_keys = keys[1..].to_vec();
-            return Some((potential_new_field, remaining_keys));
-        }
-
-        None
-    }
-
-    /// Handle JSON value field override
-    fn handle_json_value_override(
-        entry_dict: &mut Dict,
-        json_field: &str,
-        json_keys: Vec<String>,
-        value: FigmentValue,
-    ) {
-        // Ensure the JSON value field exists
-        entry_dict
-            .entry(json_field.to_string())
-            .or_insert_with(|| FigmentValue::Dict(Tag::Default, Dict::new()));
-
-        // Get or create the JSON field as a Dict
-        if let Some(json_value) = entry_dict.get_mut(json_field) {
-            let json_dict = match json_value {
-                FigmentValue::Dict(_, dict) => dict,
-                _ => {
-                    *json_value = FigmentValue::Dict(Tag::Default, Dict::new());
-                    if let FigmentValue::Dict(_, dict) = json_value {
-                        dict
-                    } else {
-                        return;
-                    }
-                }
-            };
-
-            Self::set_nested_json_field(json_dict, &json_keys, value);
-        }
-    }
-
-    /// Set nested JSON field value
-    fn set_nested_json_field(dict: &mut Dict, keys: &[String], value: FigmentValue) {
-        if keys.is_empty() {
-            return;
-        }
-
-        // Always try the full field name first (with underscores)
-        let full_field_name = keys.join("_");
-        dict.insert(full_field_name, value);
-    }
-
-    /// Navigate to a specific dictionary in the nested structure
-    fn navigate_to_dictionary<'a>(target: &'a mut Dict, path: &[String]) -> Option<&'a mut Dict> {
-        if path.is_empty() {
-            return Some(target);
-        }
-
-        let mut current = target;
-        let mut combined_keys = Vec::new();
-
-        for key in path {
-            combined_keys.push(key.clone());
-            let key_to_check = combined_keys.join("_");
-
-            current
-                .entry(key_to_check.clone())
-                .or_insert_with(|| FigmentValue::Dict(Tag::Default, Dict::new()));
-
-            match current.get_mut(&key_to_check) {
-                Some(FigmentValue::Dict(_, inner)) => {
-                    current = inner;
-                    combined_keys.clear();
-                }
-                _ => return None,
-            }
-        }
-
-        Some(current)
-    }
-
-    /// Find source array value for copying to target
-    fn find_source_array(source: &Dict, path: &[String], array_key: &str) -> Option<FigmentValue> {
-        if path.is_empty() {
-            return None;
-        }
-
-        let mut current = source;
-        let mut combined_keys = Vec::new();
-
-        for key in &path[..path.len() - 1] {
-            combined_keys.push(key.clone());
-            let key_to_check = combined_keys.join("_");
-
-            match current.get(&key_to_check) {
-                Some(FigmentValue::Dict(_, inner)) => {
-                    current = inner;
-                    combined_keys.clear();
-                }
-                _ => return None,
-            }
-        }
-
-        current.get(array_key).cloned()
-    }
-
-    /// Convert TOML value to Figment value
-    fn toml_to_figment_value(toml_value: &TomlValue) -> FigmentValue {
-        match toml_value {
-            TomlValue::String(s) => FigmentValue::from(s.clone()),
-            TomlValue::Integer(i) => FigmentValue::from(*i),
-            TomlValue::Float(f) => FigmentValue::from(*f),
-            TomlValue::Boolean(b) => FigmentValue::from(*b),
-            TomlValue::Array(arr) => {
-                let vec: Vec<FigmentValue> = arr.iter().map(Self::toml_to_figment_value).collect();
-                FigmentValue::from(vec)
-            }
-            TomlValue::Table(tbl) => {
-                let mut dict = Dict::new();
-                for (key, value) in tbl.iter() {
-                    dict.insert(key.clone(), Self::toml_to_figment_value(value));
-                }
-                FigmentValue::from(dict)
-            }
-            TomlValue::Datetime(_) => {
-                // For now, convert datetime to string representation
-                FigmentValue::from(toml_value.to_string())
-            }
-        }
-    }
-
-    fn parse_environment_value_typed(
-        value: &str,
-        expected_type: Option<&FigmentValue>,
-    ) -> FigmentValue {
-        if value.starts_with('[') && value.ends_with(']') {
-            let inner_value = value.trim_start_matches('[').trim_end_matches(']');
-            let element_type = match expected_type {
-                Some(FigmentValue::Array(_, arr)) => arr.first(),
-                _ => None,
-            };
-            let values: Vec<FigmentValue> = split_array_elements(inner_value)
-                .into_iter()
-                .map(|s| Self::parse_environment_value_typed(strip_quotes(s), element_type))
-                .collect();
-            return FigmentValue::from(values);
-        }
-
-        if let Some(FigmentValue::String(..)) = expected_type {
-            return FigmentValue::from(value);
-        }
-
-        match value.to_lowercase().as_str() {
-            "true" => return FigmentValue::from(true),
-            "false" => return FigmentValue::from(false),
-            _ => {}
-        }
-
-        if let Ok(uint_val) = value.parse::<u64>() {
-            return FigmentValue::from(uint_val);
-        }
-        if let Ok(int_val) = value.parse::<i64>() {
-            return FigmentValue::from(int_val);
-        }
-
-        if let Ok(float_val) = value.parse::<f64>() {
-            return FigmentValue::from(float_val);
-        }
-
-        FigmentValue::from(value)
-    }
-
-    fn find_expected_type<'a>(source: &'a Dict, keys: &[String]) -> Option<&'a FigmentValue> {
-        if keys.is_empty() {
-            return None;
-        }
-
-        let mut current = source;
-        let mut combined_keys = Vec::new();
-
-        for (i, key) in keys.iter().enumerate() {
-            combined_keys.push(key.clone());
-            let key_to_check = combined_keys.join("_");
-
-            match current.get(&key_to_check) {
-                Some(FigmentValue::Dict(_, inner)) => {
-                    current = inner;
-                    combined_keys.clear();
-                }
-                Some(value) if i == keys.len() - 1 => {
-                    return Some(value);
-                }
-                _ => continue,
-            }
-        }
-
-        let final_key = combined_keys.join("_");
-        current.get(&final_key)
-    }
-}
-
-fn split_array_elements(s: &str) -> Vec<&str> {
-    let mut elements = Vec::new();
-    let mut start = 0;
-    let mut in_quotes = false;
-    for (i, c) in s.char_indices() {
-        match c {
-            '"' => in_quotes = !in_quotes,
-            ',' if !in_quotes => {
-                elements.push(s[start..i].trim());
-                start = i + 1;
-            }
-            _ => {}
-        }
-    }
-    if start < s.len() {
-        elements.push(s[start..].trim());
-    }
-    elements
-}
-
-fn strip_quotes(s: &str) -> &str {
-    s.trim_matches('"')
-}
-
-pub fn parse_env_value_to_json(value: &str) -> serde_json::Value {
-    if value.starts_with('[') && value.ends_with(']') {
-        let inner = value.trim_start_matches('[').trim_end_matches(']');
-        let values: Vec<serde_json::Value> = split_array_elements(inner)
-            .into_iter()
-            .map(|s| parse_env_value_to_json(strip_quotes(s)))
-            .collect();
-        return serde_json::Value::Array(values);
-    }
-
-    match value.to_lowercase().as_str() {
-        "true" => return serde_json::Value::Bool(true),
-        "false" => return serde_json::Value::Bool(false),
-        _ => {}
-    }
-
-    if let Ok(n) = value.parse::<u64>() {
-        return serde_json::Value::Number(n.into());
-    }
-    if let Ok(n) = value.parse::<i64>() {
-        return serde_json::Value::Number(n.into());
-    }
-    if let Ok(n) = value.parse::<f64>()
-        && let Some(num) = serde_json::Number::from_f64(n)
-    {
-        return serde_json::Value::Number(num);
-    }
-
-    serde_json::Value::String(value.to_owned())
-}
-
-fn file_exists<P: AsRef<Path>>(path: P) -> bool {
-    let path = path.as_ref();
-
-    if path.is_absolute() {
-        return path.is_file();
-    }
-
-    let cwd = match std::env::current_dir() {
-        Ok(dir) => dir,
-        Err(_) => return false,
-    };
-
-    let mut current_dir = cwd.as_path();
-    loop {
-        let file_path = current_dir.join(path);
-        if file_path.is_file() {
-            return true;
-        }
-
-        current_dir = match current_dir.parent() {
-            Some(parent) => parent,
-            None => return false,
-        };
-    }
-}
-
-impl<T: ConfigurationType, P: Provider + Clone> ConfigProvider<T> for FileConfigProvider<P> {
-    async fn load_config(&self) -> Result<T, ConfigurationError> {
-        info!("Loading config from path: '{}'...", self.file_path);
-
-        // Start with the default configuration if provided
-        let mut config_builder = Figment::new();
-        let has_default = self.default_config.is_some();
-        if let Some(default) = &self.default_config {
-            config_builder = config_builder.merge(default);
-        } else {
-            warn!("No default configuration provided.");
-        }
-
-        // If the config file exists, merge it into the configuration
-        if file_exists(&self.file_path) {
-            info!("Found configuration file at path: '{}'.", self.file_path);
-            config_builder = config_builder.merge(Toml::file(&self.file_path));
-        } else {
-            warn!(
-                "Configuration file not found at path: '{}'.",
-                self.file_path
-            );
-            if has_default {
-                info!(
-                    "Using default configuration embedded into server, as no config file was found."
-                );
-            }
-        }
-
-        // Merge environment variables into the configuration
-        config_builder = config_builder.merge(self.env_provider.clone());
-
-        // Finally, attempt to extract the final configuration
-        let config_result: Result<T, figment::Error> = config_builder.extract();
-
-        match config_result {
-            Ok(config) => {
-                info!("Config loaded successfully.");
-                let display_config = env::var(DISPLAY_CONFIG_ENV)
-                    .map(|val| val == "1" || val.to_lowercase() == "true")
-                    .unwrap_or(self.display_config);
-                if display_config {
-                    info!("Using Config: {config}");
-                }
-                Ok(config)
-            }
-            Err(e) => {
-                error!("Failed to load config: {e}");
-                Err(ConfigurationError::CannotLoadConfiguration)
-            }
-        }
-    }
-}
diff --git a/core/common/src/lib.rs b/core/common/src/lib.rs
index 9171c4097..f5a955ac2 100644
--- a/core/common/src/lib.rs
+++ b/core/common/src/lib.rs
@@ -18,7 +18,6 @@
 mod alloc;
 mod certificates;
 mod commands;
-mod configs;
 mod error;
 mod sender;
 pub mod sharding;
@@ -44,7 +43,7 @@ pub use commands::system::get_cluster_metadata::*;
 pub use commands::system::*;
 pub use commands::topics::*;
 pub use commands::users::*;
-pub use configs::*;
+pub use configs_derive::ConfigEnv;
 pub use sender::{
     QuicSender, Sender, SenderKind, TcpSender, TcpTlsSender, WebSocketSender, WebSocketTlsSender,
 };
diff --git a/core/configs/Cargo.toml b/core/configs/Cargo.toml
new file mode 100644
index 000000000..d03f44c04
--- /dev/null
+++ b/core/configs/Cargo.toml
@@ -0,0 +1,30 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+[package]
+name = "configs"
+version = "0.1.0"
+edition = "2024"
+license = "Apache-2.0"
+publish = false
+
+[dependencies]
+configs_derive = { workspace = true }
+figment = { workspace = true }
+serde = { workspace = true }
+serde_json = { workspace = true }
+tracing = { workspace = true }
diff --git a/core/configs/src/configs_impl/env_mapping.rs b/core/configs/src/configs_impl/env_mapping.rs
new file mode 100644
index 000000000..6cb93f7d6
--- /dev/null
+++ b/core/configs/src/configs_impl/env_mapping.rs
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+//! Environment variable to config path mapping types.
+
+/// Represents a single environment variable to config path mapping.
+/// Used by the `ConfigEnv` derive macro to generate compile-time mappings.
+#[derive(Debug, Clone, Copy)]
+pub struct EnvVarMapping {
+    /// The environment variable name (e.g., "IGGY_HTTP_ENABLED")
+    pub env_name: &'static str,
+    /// The config path (e.g., "http.enabled")
+    pub config_path: &'static str,
+    /// Whether this field contains secret data
+    pub is_secret: bool,
+}
+
+/// Trait for configuration types that provide environment variable mappings.
+/// Implemented automatically by the `#[derive(ConfigEnv)]` macro.
+pub trait ConfigEnvMappings {
+    /// Returns all environment variable mappings for this config type.
+    fn env_mappings() -> &'static [EnvVarMapping];
+
+    /// Finds a mapping by environment variable name.
+    fn find_by_env_name(env_name: &str) -> Option<&'static EnvVarMapping> {
+        Self::env_mappings().iter().find(|m| m.env_name == env_name)
+    }
+
+    /// Finds a mapping by config path.
+    fn find_by_config_path(path: &str) -> Option<&'static EnvVarMapping> {
+        Self::env_mappings().iter().find(|m| m.config_path == path)
+    }
+
+    /// Returns all valid environment variable names.
+    fn all_env_var_names() -> Vec<&'static str> {
+        Self::env_mappings().iter().map(|m| m.env_name).collect()
+    }
+
+    /// Returns environment variable names marked as secrets.
+    fn secret_env_names() -> Vec<&'static str> {
+        Self::env_mappings()
+            .iter()
+            .filter(|m| m.is_secret)
+            .map(|m| m.env_name)
+            .collect()
+    }
+}
diff --git a/core/configs/src/configs_impl/error.rs b/core/configs/src/configs_impl/error.rs
new file mode 100644
index 000000000..1b36e89b1
--- /dev/null
+++ b/core/configs/src/configs_impl/error.rs
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+//! Configuration error types.
+
+use std::fmt::Display;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum ConfigurationError {
+    CannotLoadConfiguration,
+    DefaultSerializationFailed,
+    DefaultParsingFailed,
+    EnvironmentVariableParsingFailed,
+    InvalidConfigurationValue,
+}
+
+impl Display for ConfigurationError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::CannotLoadConfiguration => write!(f, "Cannot load configuration"),
+            Self::DefaultSerializationFailed => {
+                write!(f, "Failed to serialize default configuration")
+            }
+            Self::DefaultParsingFailed => write!(f, "Failed to parse default configuration"),
+            Self::EnvironmentVariableParsingFailed => {
+                write!(f, "Failed to parse environment variables")
+            }
+            Self::InvalidConfigurationValue => {
+                write!(f, "Provided configuration value is invalid")
+            }
+        }
+    }
+}
+
+impl std::error::Error for ConfigurationError {}
diff --git a/core/configs/src/configs_impl/file_provider.rs b/core/configs/src/configs_impl/file_provider.rs
new file mode 100644
index 000000000..87463e38c
--- /dev/null
+++ b/core/configs/src/configs_impl/file_provider.rs
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+//! File-based configuration provider.
+
+use super::error::ConfigurationError;
+use super::traits::{ConfigProvider, ConfigurationType};
+use figment::{
+    Figment, Provider,
+    providers::{Data, Format, Toml},
+};
+use std::{env, path::Path};
+use tracing::{error, info, warn};
+
+const DISPLAY_CONFIG_ENV: &str = "IGGY_DISPLAY_CONFIG";
+
+/// File-based configuration provider that combines file, default, and environment configurations.
+pub struct FileConfigProvider<P> {
+    file_path: String,
+    default_config: Option<Data<Toml>>,
+    env_provider: P,
+    display_config: bool,
+}
+
+impl<P: Provider> FileConfigProvider<P> {
+    /// Create a new file configuration provider.
+    ///
+    /// # Arguments
+    /// * `file_path` - Path to the configuration file
+    /// * `env_provider` - Environment variable provider
+    /// * `display_config` - Whether to display the loaded configuration
+    /// * `default_config` - Optional default configuration data
+    pub fn new(
+        file_path: String,
+        env_provider: P,
+        display_config: bool,
+        default_config: Option<Data<Toml>>,
+    ) -> Self {
+        Self {
+            file_path,
+            env_provider,
+            default_config,
+            display_config,
+        }
+    }
+}
+
+impl<P: Provider + Clone> ConfigProvider for FileConfigProvider<P> {
+    async fn load_config<T: ConfigurationType>(&self) -> Result<T, ConfigurationError> {
+        info!("Loading config from path: '{}'...", self.file_path);
+
+        // Start with the default configuration if provided
+        let mut config_builder = Figment::new();
+        let has_default = self.default_config.is_some();
+        if let Some(default) = &self.default_config {
+            config_builder = config_builder.merge(default);
+        } else {
+            warn!("No default configuration provided.");
+        }
+
+        // If the config file exists, merge it into the configuration
+        if file_exists(&self.file_path) {
+            info!("Found configuration file at path: '{}'.", self.file_path);
+            config_builder = config_builder.merge(Toml::file(&self.file_path));
+        } else {
+            warn!(
+                "Configuration file not found at path: '{}'.",
+                self.file_path
+            );
+            if has_default {
+                info!(
+                    "Using default configuration embedded into server, as no config file was found."
+                );
+            }
+        }
+
+        // Merge environment variables into the configuration
+        config_builder = config_builder.merge(self.env_provider.clone());
+
+        // Finally, attempt to extract the final configuration
+        let config_result: Result<T, figment::Error> = config_builder.extract();
+
+        match config_result {
+            Ok(config) => {
+                info!("Config loaded successfully.");
+                let display_config = env::var(DISPLAY_CONFIG_ENV)
+                    .map(|val| val == "1" || val.to_lowercase() == "true")
+                    .unwrap_or(self.display_config);
+                if display_config {
+                    info!("Using Config: {config}");
+                }
+                Ok(config)
+            }
+            Err(e) => {
+                error!("Failed to load config: {e}");
+                Err(ConfigurationError::CannotLoadConfiguration)
+            }
+        }
+    }
+}
+
+fn file_exists<P: AsRef<Path>>(path: P) -> bool {
+    let path = path.as_ref();
+
+    if path.is_absolute() {
+        return path.is_file();
+    }
+
+    let cwd = match std::env::current_dir() {
+        Ok(dir) => dir,
+        Err(_) => return false,
+    };
+
+    let mut current_dir = cwd.as_path();
+    loop {
+        let file_path = current_dir.join(path);
+        if file_path.is_file() {
+            return true;
+        }
+
+        current_dir = match current_dir.parent() {
+            Some(parent) => parent,
+            None => return false,
+        };
+    }
+}
diff --git a/core/configs/src/configs_impl/mod.rs b/core/configs/src/configs_impl/mod.rs
new file mode 100644
index 000000000..7c2f23738
--- /dev/null
+++ b/core/configs/src/configs_impl/mod.rs
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+//! Configuration module providing flexible configuration loading with file and environment support.
+//!
+//! This module provides a trait-based configuration system that supports:
+//! - Loading configuration from TOML files
+//! - Environment variable overrides with complex path resolution
+//! - Array and HashMap field overrides
+//! - JSON value field handling
+//! - Automatic type conversion and validation
+
+mod env_mapping;
+mod error;
+mod file_provider;
+mod parsing;
+mod traits;
+mod typed_env_provider;
+
+pub use env_mapping::{ConfigEnvMappings, EnvVarMapping};
+pub use error::ConfigurationError;
+pub use file_provider::FileConfigProvider;
+pub use parsing::parse_env_value_to_json;
+pub use traits::{ConfigProvider, ConfigurationType};
+pub use typed_env_provider::TypedEnvProvider;
diff --git a/core/configs/src/configs_impl/parsing.rs b/core/configs/src/configs_impl/parsing.rs
new file mode 100644
index 000000000..d4675d7d3
--- /dev/null
+++ b/core/configs/src/configs_impl/parsing.rs
@@ -0,0 +1,196 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+//! Environment variable value parsing utilities.
+
+use figment::value::Value as FigmentValue;
+
+/// Parse an environment variable value into a FigmentValue with type inference.
+pub(crate) fn parse_env_value(value: &str) -> FigmentValue {
+    // Handle array syntax [a, b, c]
+    if value.starts_with('[') && value.ends_with(']') {
+        let inner = value.trim_start_matches('[').trim_end_matches(']');
+        let elements: Vec<FigmentValue> = split_array_elements(inner)
+            .into_iter()
+            .map(|s| parse_env_value(strip_quotes(s)))
+            .collect();
+        return FigmentValue::from(elements);
+    }
+
+    // Boolean
+    match value.to_lowercase().as_str() {
+        "true" => return FigmentValue::from(true),
+        "false" => return FigmentValue::from(false),
+        _ => {}
+    }
+
+    // Try numeric types
+    if let Ok(n) = value.parse::<u64>() {
+        return FigmentValue::from(n);
+    }
+    if let Ok(n) = value.parse::<i64>() {
+        return FigmentValue::from(n);
+    }
+    if let Ok(n) = value.parse::<f64>() {
+        return FigmentValue::from(n);
+    }
+
+    // Default to string
+    FigmentValue::from(value)
+}
+
+/// Parse an environment variable value into a JSON value with type inference.
+pub fn parse_env_value_to_json(value: &str) -> serde_json::Value {
+    if value.starts_with('[') && value.ends_with(']') {
+        let inner = value.trim_start_matches('[').trim_end_matches(']');
+        let values: Vec<serde_json::Value> = split_array_elements(inner)
+            .into_iter()
+            .map(|s| parse_env_value_to_json(strip_quotes(s)))
+            .collect();
+        return serde_json::Value::Array(values);
+    }
+
+    match value.to_lowercase().as_str() {
+        "true" => return serde_json::Value::Bool(true),
+        "false" => return serde_json::Value::Bool(false),
+        _ => {}
+    }
+
+    if let Ok(n) = value.parse::<u64>() {
+        return serde_json::Value::Number(n.into());
+    }
+    if let Ok(n) = value.parse::<i64>() {
+        return serde_json::Value::Number(n.into());
+    }
+    if let Ok(n) = value.parse::<f64>()
+        && let Some(num) = serde_json::Number::from_f64(n)
+    {
+        return serde_json::Value::Number(num);
+    }
+
+    serde_json::Value::String(value.to_owned())
+}
+
+pub(crate) fn split_array_elements(s: &str) -> Vec<&str> {
+    let mut elements = Vec::new();
+    let mut start = 0;
+    let mut in_quotes = false;
+    for (i, c) in s.char_indices() {
+        match c {
+            '"' => in_quotes = !in_quotes,
+            ',' if !in_quotes => {
+                elements.push(s[start..i].trim());
+                start = i + 1;
+            }
+            _ => {}
+        }
+    }
+    if start < s.len() {
+        elements.push(s[start..].trim());
+    }
+    elements
+}
+
+pub(crate) fn strip_quotes(s: &str) -> &str {
+    s.trim_matches('"')
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn parse_env_value_to_json_handles_booleans() {
+        assert_eq!(
+            parse_env_value_to_json("true"),
+            serde_json::Value::Bool(true)
+        );
+        assert_eq!(
+            parse_env_value_to_json("TRUE"),
+            serde_json::Value::Bool(true)
+        );
+        assert_eq!(
+            parse_env_value_to_json("false"),
+            serde_json::Value::Bool(false)
+        );
+        assert_eq!(
+            parse_env_value_to_json("False"),
+            serde_json::Value::Bool(false)
+        );
+    }
+
+    #[test]
+    fn parse_env_value_to_json_handles_integers() {
+        assert_eq!(parse_env_value_to_json("42"), serde_json::json!(42));
+        assert_eq!(parse_env_value_to_json("0"), serde_json::json!(0));
+        assert_eq!(parse_env_value_to_json("-123"), serde_json::json!(-123));
+    }
+
+    #[test]
+    fn parse_env_value_to_json_handles_floats() {
+        assert_eq!(parse_env_value_to_json("1.5"), serde_json::json!(1.5));
+        assert_eq!(parse_env_value_to_json("-2.5"), serde_json::json!(-2.5));
+    }
+
+    #[test]
+    fn parse_env_value_to_json_handles_strings() {
+        assert_eq!(parse_env_value_to_json("hello"), serde_json::json!("hello"));
+        assert_eq!(
+            parse_env_value_to_json("127.0.0.1:8080"),
+            serde_json::json!("127.0.0.1:8080")
+        );
+    }
+
+    #[test]
+    fn parse_env_value_to_json_handles_arrays() {
+        assert_eq!(
+            parse_env_value_to_json("[1, 2, 3]"),
+            serde_json::json!([1, 2, 3])
+        );
+        assert_eq!(
+            parse_env_value_to_json("[\"a\", \"b\"]"),
+            serde_json::json!(["a", "b"])
+        );
+        assert_eq!(
+            parse_env_value_to_json("[true, false]"),
+            serde_json::json!([true, false])
+        );
+    }
+
+    #[test]
+    fn split_array_elements_handles_simple_arrays() {
+        assert_eq!(split_array_elements("a, b, c"), vec!["a", "b", "c"]);
+        assert_eq!(split_array_elements("1,2,3"), vec!["1", "2", "3"]);
+    }
+
+    #[test]
+    fn split_array_elements_handles_quoted_strings() {
+        assert_eq!(
+            split_array_elements("\"hello, world\", \"foo\""),
+            vec!["\"hello, world\"", "\"foo\""]
+        );
+    }
+
+    #[test]
+    fn strip_quotes_removes_surrounding_quotes() {
+        assert_eq!(strip_quotes("\"hello\""), "hello");
+        assert_eq!(strip_quotes("hello"), "hello");
+        assert_eq!(strip_quotes("\"\""), "");
+    }
+}
diff --git a/core/configs/src/configs_impl/traits.rs b/core/configs/src/configs_impl/traits.rs
new file mode 100644
index 000000000..213fa42ce
--- /dev/null
+++ b/core/configs/src/configs_impl/traits.rs
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+//! Configuration provider traits.
+
+use super::error::ConfigurationError;
+use serde::{Serialize, de::DeserializeOwned};
+use std::fmt::Display;
+use std::future::Future;
+
+/// Type alias for configuration that can be serialized, deserialized, has defaults, and can be displayed.
+pub trait ConfigurationType: Serialize + DeserializeOwned + Default + Display {}
+impl<T: Serialize + DeserializeOwned + Default + Display> ConfigurationType for T {}
+
+/// Trait for configuration providers that can load a configuration.
+pub trait ConfigProvider {
+    fn load_config<T: ConfigurationType>(
+        &self,
+    ) -> impl Future<Output = Result<T, ConfigurationError>>;
+}
diff --git a/core/configs/src/configs_impl/typed_env_provider.rs b/core/configs/src/configs_impl/typed_env_provider.rs
new file mode 100644
index 000000000..dd8926985
--- /dev/null
+++ b/core/configs/src/configs_impl/typed_env_provider.rs
@@ -0,0 +1,597 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use super::env_mapping::ConfigEnvMappings;
+use super::error::ConfigurationError;
+use super::parsing::parse_env_value;
+use figment::{
+    Profile, Provider,
+    value::{Dict, Map as FigmentMap, Tag, Value as FigmentValue},
+};
+use std::{collections::HashSet, env, marker::PhantomData};
+use tracing::{info, warn};
+
+const SECRET_MASK: &str = "******";
+
+/// Controls how env var names are resolved during deserialization.
+enum EnvNameResolution<'a> {
+    /// Use mapping.env_name directly (for configs with compile-time prefix)
+    Direct,
+    /// Prepend runtime prefix to mapping.env_name (for plugin configs)
+    PrependPrefix(&'a str),
+}
+
+/// Controls filtering and messaging for unknown env var warnings.
+enum WarningContext<'a> {
+    /// Main config: filter IGNORED_ENV_VARS and DELEGATED prefixes
+    MainConfig,
+    /// Connector config: filter PLUGIN_CONFIG_ prefix
+    ConnectorConfig(&'a str),
+}
+
+/// Environment variables starting with IGGY_ that are NOT config values.
+/// These are used for test control, CI, CLI behavior, config file paths, etc.
+const IGNORED_ENV_VARS: &[&str] = &[
+    "IGGY_CI_BUILD",
+    "IGGY_CONFIG_PATH",
+    "IGGY_CONNECTORS_CONFIG_PATH",
+    "IGGY_MCP_CONFIG_PATH",
+    "IGGY_ROOT_PASSWORD",
+    "IGGY_ROOT_USERNAME",
+    "IGGY_TEST_VERBOSE",
+];
+
+/// Prefixes for env vars handled by separate providers with runtime prefixes.
+/// The main config provider skips these; each sub-provider validates its own vars.
+const DELEGATED_ENV_VAR_PREFIXES: &[&str] = &["IGGY_CONNECTORS_SINK_", "IGGY_CONNECTORS_SOURCE_"];
+
+type ProfileMap = FigmentMap<Profile, Dict>;
+
+/// Type-safe environment variable provider that uses compile-time generated mappings.
+///
+/// Unlike runtime schema inference providers, this provider uses the `ConfigEnvMappings`
+/// trait to directly look up known env var names, eliminating ambiguity in path resolution.
+///
+/// # Example
+/// ```ignore
+/// let provider = TypedEnvProvider::<ServerConfig>::new("IGGY_", &["IGGY_SYSTEM_ENCRYPTION_KEY"]);
+/// ```
+#[derive(Debug, Clone)]
+pub struct TypedEnvProvider<T: ConfigEnvMappings> {
+    prefix: String,
+    secret_keys: Vec<String>,
+    _phantom: PhantomData<T>,
+}
+
+impl<T: ConfigEnvMappings> TypedEnvProvider<T> {
+    /// Create a new typed environment provider.
+    ///
+    /// # Arguments
+    /// * `prefix` - Environment variable prefix to validate against (e.g., "IGGY_")
+    /// * `secret_keys` - Environment variable names that should be masked in logs
+    pub fn new(prefix: &str, secret_keys: &[&str]) -> Self {
+        Self {
+            prefix: prefix.to_string(),
+            secret_keys: secret_keys.iter().map(|s| s.to_string()).collect(),
+            _phantom: PhantomData,
+        }
+    }
+
+    /// Create a provider with a runtime-determined prefix.
+    ///
+    /// Use this when the config struct has no prefix attribute (generates relative mappings)
+    /// but you need to apply a prefix at runtime.
+    ///
+    /// # Example
+    /// ```ignore
+    /// // SinkConfig has no prefix, generates: ENABLED, NAME, PATH
+    /// // We prepend "IGGY_CONNECTORS_SINK_POSTGRES_" at runtime
+    /// let provider = TypedEnvProvider::<SinkConfig>::with_runtime_prefix(
+    ///     "IGGY_CONNECTORS_SINK_POSTGRES_",
+    ///     &[]
+    /// );
+    /// ```
+    pub fn with_runtime_prefix(prefix: &str, secret_keys: &[&str]) -> Self {
+        Self::new(prefix, secret_keys)
+    }
+
+    /// Create a provider with default prefix "IGGY_" and no secret keys.
+    pub fn with_default_prefix() -> Self {
+        Self::new("IGGY_", &[])
+    }
+
+    /// Create a provider using auto-collected secrets from `#[config_env(secret)]` markers.
+    pub fn from_config(prefix: &str) -> Self {
+        let secret_keys: Vec<String> = T::secret_env_names()
+            .into_iter()
+            .map(String::from)
+            .collect();
+        Self {
+            prefix: prefix.to_string(),
+            secret_keys,
+            _phantom: PhantomData,
+        }
+    }
+
+    /// Deserialize with runtime prefix prepended to each mapping's env_name.
+    ///
+    /// Unlike `deserialize()`, this method prepends `self.prefix` to each mapping's
+    /// env_name, allowing for dynamic prefix construction at runtime.
+    pub fn deserialize_with_runtime_prefix(&self) -> Result<ProfileMap, ConfigurationError> {
+        self.warn_unknown_env_vars_inner(WarningContext::ConnectorConfig(&self.prefix));
+        self.deserialize_inner(EnvNameResolution::PrependPrefix(&self.prefix))
+    }
+
+    /// Deserialize environment variables into a configuration profile map.
+    ///
+    /// This method:
+    /// 1. Validates that all env vars with the prefix are known (warns on unknown)
+    /// 2. Iterates over compile-time generated mappings and applies set values
+    pub fn deserialize(&self) -> Result<ProfileMap, ConfigurationError> {
+        self.warn_unknown_env_vars_inner(WarningContext::MainConfig);
+        self.deserialize_inner(EnvNameResolution::Direct)
+    }
+
+    fn deserialize_inner(
+        &self,
+        resolution: EnvNameResolution,
+    ) -> Result<ProfileMap, ConfigurationError> {
+        let mut root_dict = Dict::new();
+
+        for mapping in T::env_mappings() {
+            let env_name = match &resolution {
+                EnvNameResolution::Direct => mapping.env_name.to_string(),
+                EnvNameResolution::PrependPrefix(prefix) => {
+                    format!("{}{}", prefix, mapping.env_name)
+                }
+            };
+
+            let env_value = match env::var(&env_name) {
+                Ok(val) => val,
+                Err(_) => continue,
+            };
+
+            let is_secret = mapping.is_secret
+                || match &resolution {
+                    EnvNameResolution::Direct => self.secret_keys.contains(&env_name),
+                    EnvNameResolution::PrependPrefix(_) => {
+                        self.secret_keys.iter().any(|s| env_name.ends_with(s))
+                    }
+                };
+
+            let display_value = if is_secret {
+                SECRET_MASK.to_string()
+            } else {
+                env_value.clone()
+            };
+
+            info!(
+                "{} value changed to: {} from environment variable",
+                env_name, display_value
+            );
+
+            let parsed_value = parse_env_value(&env_value);
+            Self::insert_at_path(&mut root_dict, mapping.config_path, parsed_value);
+        }
+
+        let root_dict =
+            Self::convert_numeric_dicts_to_arrays(FigmentValue::Dict(Tag::Default, root_dict));
+        let root_dict = match root_dict {
+            FigmentValue::Dict(_, dict) => dict,
+            _ => Dict::new(),
+        };
+
+        let mut data = ProfileMap::new();
+        data.insert(Profile::default(), root_dict);
+        Ok(data)
+    }
+
+    /// Recursively convert dicts with all numeric string keys to arrays.
+    fn convert_numeric_dicts_to_arrays(value: FigmentValue) -> FigmentValue {
+        match value {
+            FigmentValue::Dict(tag, dict) => {
+                let processed: Dict = dict
+                    .into_iter()
+                    .map(|(k, v)| (k, Self::convert_numeric_dicts_to_arrays(v)))
+                    .collect();
+
+                if Self::is_array_dict(&processed) {
+                    Self::dict_to_array(processed)
+                } else {
+                    FigmentValue::Dict(tag, processed)
+                }
+            }
+            FigmentValue::Array(tag, arr) => {
+                let processed: Vec<FigmentValue> = arr
+                    .into_iter()
+                    .map(Self::convert_numeric_dicts_to_arrays)
+                    .collect();
+                FigmentValue::Array(tag, processed)
+            }
+            other => other,
+        }
+    }
+
+    fn is_array_dict(dict: &Dict) -> bool {
+        if dict.is_empty() {
+            return false;
+        }
+        dict.keys().all(|k| k.parse::<usize>().is_ok())
+    }
+
+    fn dict_to_array(dict: Dict) -> FigmentValue {
+        let mut indexed: Vec<(usize, FigmentValue)> = dict
+            .into_iter()
+            .filter_map(|(k, v)| k.parse::<usize>().ok().map(|i| (i, v)))
+            .collect();
+
+        indexed.sort_by_key(|(i, _)| *i);
+
+        let max_index = indexed.last().map(|(i, _)| *i).unwrap_or(0);
+        let mut array = vec![FigmentValue::Dict(Tag::Default, Dict::new()); max_index + 1];
+
+        for (i, v) in indexed {
+            array[i] = v;
+        }
+
+        FigmentValue::Array(Tag::Default, array)
+    }
+
+    fn warn_unknown_env_vars_inner(&self, context: WarningContext) {
+        let known_vars: HashSet<String> = match &context {
+            WarningContext::MainConfig => T::env_mappings()
+                .iter()
+                .map(|m| m.env_name.to_string())
+                .collect(),
+            WarningContext::ConnectorConfig(prefix) => T::env_mappings()
+                .iter()
+                .map(|m| format!("{}{}", prefix, m.env_name))
+                .collect(),
+        };
+        let known_refs: HashSet<&str> = known_vars.iter().map(|s| s.as_str()).collect();
+
+        for (key, _) in env::vars() {
+            if !key.starts_with(&self.prefix) {
+                continue;
+            }
+
+            if known_vars.contains(&key) {
+                continue;
+            }
+
+            let should_skip = match &context {
+                WarningContext::MainConfig => {
+                    IGNORED_ENV_VARS.contains(&key.as_str())
+                        || DELEGATED_ENV_VAR_PREFIXES
+                            .iter()
+                            .any(|p| key.starts_with(p))
+                }
+                WarningContext::ConnectorConfig(prefix) => {
+                    let plugin_config_prefix = format!("{}PLUGIN_CONFIG_", prefix);
+                    key.starts_with(&plugin_config_prefix)
+                }
+            };
+            if should_skip {
+                continue;
+            }
+
+            let suggestions = Self::find_similar_vars(&key, &known_refs);
+            let suggestion_hint = if suggestions.is_empty() {
+                String::new()
+            } else {
+                format!(" Similar: {}", suggestions.join(", "))
+            };
+
+            let debug_msg = match &context {
+                WarningContext::MainConfig => format!(
+                    "Unknown IGGY_ env var: '{}'.{}. Add to IGNORED_ENV_VARS if intentional, \
+                     or add #[derive(ConfigEnv)] to the config struct.",
+                    key, suggestion_hint
+                ),
+                WarningContext::ConnectorConfig(_) => format!(
+                    "Unknown connector env var: '{}'.{} Check field name or add #[config_env(skip)].",
+                    key, suggestion_hint
+                ),
+            };
+            debug_assert!(false, "{}", debug_msg);
+
+            Self::warn_unknown_var(&key, &suggestions);
+        }
+    }
+
+    fn warn_unknown_var(unknown_var: &str, suggestions: &[String]) {
+        if suggestions.is_empty() {
+            warn!(
+                "Unknown environment variable '{}' will be ignored. \
+                 Use --list-env-vars to see all valid environment variables.",
+                unknown_var
+            );
+        } else {
+            warn!(
+                "Unknown environment variable '{}' will be ignored. Similar variables: {}?",
+                unknown_var,
+                suggestions.join(", ")
+            );
+        }
+    }
+
+    /// Find similar variable names using Levenshtein edit distance.
+    /// Returns up to 3 suggestions sorted by similarity.
+    pub fn find_similar_vars(
+        unknown: &str,
+        known: &std::collections::HashSet<&str>,
+    ) -> Vec<String> {
+        let unknown_lower = unknown.to_lowercase();
+        let mut suggestions: Vec<(String, usize)> = known
+            .iter()
+            .filter_map(|&known_var| {
+                let known_lower = known_var.to_lowercase();
+                let distance = Self::levenshtein_distance(&unknown_lower, &known_lower);
+                let threshold = (unknown.len().max(known_var.len()) * 3) / 10;
+                if distance <= threshold.max(3) {
+                    Some((known_var.to_string(), distance))
+                } else {
+                    None
+                }
+            })
+            .collect();
+
+        suggestions.sort_by_key(|(_, dist)| *dist);
+        suggestions.truncate(3);
+        suggestions.into_iter().map(|(s, _)| s).collect()
+    }
+
+    fn levenshtein_distance(a: &str, b: &str) -> usize {
+        let a_chars: Vec<char> = a.chars().collect();
+        let b_chars: Vec<char> = b.chars().collect();
+        let a_len = a_chars.len();
+        let b_len = b_chars.len();
+
+        if a_len == 0 {
+            return b_len;
+        }
+        if b_len == 0 {
+            return a_len;
+        }
+
+        let mut prev_row: Vec<usize> = (0..=b_len).collect();
+        let mut curr_row = vec![0; b_len + 1];
+
+        for (i, a_char) in a_chars.iter().enumerate() {
+            curr_row[0] = i + 1;
+
+            for (j, b_char) in b_chars.iter().enumerate() {
+                let cost = if a_char == b_char { 0 } else { 1 };
+                curr_row[j + 1] = (prev_row[j + 1] + 1)
+                    .min(curr_row[j] + 1)
+                    .min(prev_row[j] + cost);
+            }
+
+            std::mem::swap(&mut prev_row, &mut curr_row);
+        }
+
+        prev_row[b_len]
+    }
+
+    fn insert_at_path(dict: &mut Dict, path: &str, value: FigmentValue) {
+        let segments: Vec<&str> = path.split('.').collect();
+        Self::insert_at_path_segments(dict, &segments, value);
+    }
+
+    fn insert_at_path_segments(dict: &mut Dict, segments: &[&str], value: FigmentValue) {
+        if segments.is_empty() {
+            return;
+        }
+
+        if segments.len() == 1 {
+            dict.insert(segments[0].to_string(), value);
+            return;
+        }
+
+        let key = segments[0].to_string();
+        dict.entry(key.clone())
+            .or_insert_with(|| FigmentValue::Dict(Tag::Default, Dict::new()));
+
+        if let Some(FigmentValue::Dict(_, inner_dict)) = dict.get_mut(&key) {
+            Self::insert_at_path_segments(inner_dict, &segments[1..], value);
+        }
+    }
+}
+
+impl<T: ConfigEnvMappings + Send + Sync> Provider for TypedEnvProvider<T> {
+    fn metadata(&self) -> figment::Metadata {
+        figment::Metadata::named("typed environment variables")
+    }
+
+    fn data(&self) -> Result<ProfileMap, figment::Error> {
+        self.deserialize()
+            .map_err(|e| figment::Error::from(format!("Failed to deserialize env vars: {e}")))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::ConfigEnv;
+    use serde::{Deserialize, Serialize};
+    use std::collections::HashSet;
+
+    #[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigEnv)]
+    #[config_env(prefix = "TEST_", name = "test-config")]
+    struct TestConfig {
+        enabled: bool,
+        name: String,
+        count: u32,
+        nested: NestedConfig,
+    }
+
+    #[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigEnv)]
+    struct NestedConfig {
+        value: String,
+        flag: bool,
+    }
+
+    #[test]
+    fn levenshtein_distance_identical_strings() {
+        assert_eq!(
+            TypedEnvProvider::<TestConfig>::levenshtein_distance("hello", "hello"),
+            0
+        );
+    }
+
+    #[test]
+    fn levenshtein_distance_single_char_difference() {
+        assert_eq!(
+            TypedEnvProvider::<TestConfig>::levenshtein_distance("hello", "hallo"),
+            1
+        );
+        assert_eq!(
+            TypedEnvProvider::<TestConfig>::levenshtein_distance("cat", "car"),
+            1
+        );
+    }
+
+    #[test]
+    fn levenshtein_distance_empty_strings() {
+        assert_eq!(
+            TypedEnvProvider::<TestConfig>::levenshtein_distance("", ""),
+            0
+        );
+        assert_eq!(
+            TypedEnvProvider::<TestConfig>::levenshtein_distance("abc", ""),
+            3
+        );
+        assert_eq!(
+            TypedEnvProvider::<TestConfig>::levenshtein_distance("", "abc"),
+            3
+        );
+    }
+
+    #[test]
+    fn levenshtein_distance_insertions_deletions() {
+        assert_eq!(
+            TypedEnvProvider::<TestConfig>::levenshtein_distance("abc", "ab"),
+            1
+        );
+        assert_eq!(
+            TypedEnvProvider::<TestConfig>::levenshtein_distance("ab", "abc"),
+            1
+        );
+    }
+
+    #[test]
+    fn find_similar_vars_suggests_typos() {
+        let known: HashSet<&str> = ["TEST_ENABLED", "TEST_NAME", "TEST_COUNT"]
+            .into_iter()
+            .collect();
+
+        let suggestions = TypedEnvProvider::<TestConfig>::find_similar_vars("TEST_ENABELD", &known);
+        assert!(suggestions.contains(&"TEST_ENABLED".to_string()));
+
+        let suggestions = TypedEnvProvider::<TestConfig>::find_similar_vars("TEST_NANE", &known);
+        assert!(suggestions.contains(&"TEST_NAME".to_string()));
+    }
+
+    #[test]
+    fn find_similar_vars_returns_empty_for_unrelated() {
+        let known: HashSet<&str> = ["TEST_ENABLED", "TEST_NAME"].into_iter().collect();
+
+        let suggestions =
+            TypedEnvProvider::<TestConfig>::find_similar_vars("COMPLETELY_DIFFERENT_VAR", &known);
+        assert!(suggestions.is_empty());
+    }
+
+    #[test]
+    fn find_similar_vars_limits_to_three_suggestions() {
+        let known: HashSet<&str> = ["VAR_A", "VAR_B", "VAR_C", "VAR_D", "VAR_E"]
+            .into_iter()
+            .collect();
+
+        let suggestions = TypedEnvProvider::<TestConfig>::find_similar_vars("VAR_X", &known);
+        assert!(suggestions.len() <= 3);
+    }
+
+    #[test]
+    fn test_config_generates_env_mappings() {
+        let mappings = TestConfig::env_mappings();
+        assert!(!mappings.is_empty());
+
+        assert!(mappings.iter().any(|m| m.env_name == "TEST_ENABLED"));
+        assert!(mappings.iter().any(|m| m.env_name == "TEST_NAME"));
+        assert!(mappings.iter().any(|m| m.env_name == "TEST_COUNT"));
+        assert!(mappings.iter().any(|m| m.env_name == "TEST_NESTED_VALUE"));
+        assert!(mappings.iter().any(|m| m.env_name == "TEST_NESTED_FLAG"));
+    }
+
+    #[test]
+    fn test_config_mappings_have_correct_paths() {
+        let mappings = TestConfig::env_mappings();
+
+        let enabled = mappings
+            .iter()
+            .find(|m| m.env_name == "TEST_ENABLED")
+            .unwrap();
+        assert_eq!(enabled.config_path, "enabled");
+
+        let nested_value = mappings
+            .iter()
+            .find(|m| m.env_name == "TEST_NESTED_VALUE")
+            .unwrap();
+        assert_eq!(nested_value.config_path, "nested.value");
+    }
+
+    #[test]
+    fn typed_provider_deserializes_env_vars() {
+        unsafe {
+            env::set_var("TEST_ENABLED", "true");
+            env::set_var("TEST_NAME", "test-name");
+            env::set_var("TEST_COUNT", "42");
+        }
+
+        let provider = TypedEnvProvider::<TestConfig>::new("TEST_", &[]);
+        let data = provider.deserialize().expect("deserialize failed");
+
+        unsafe {
+            env::remove_var("TEST_ENABLED");
+            env::remove_var("TEST_NAME");
+            env::remove_var("TEST_COUNT");
+        }
+
+        let profile_data = data
+            .get(&figment::Profile::default())
+            .expect("no default profile");
+
+        let enabled = profile_data.get("enabled").expect("no enabled");
+        assert!(matches!(enabled, FigmentValue::Bool(_, true)));
+
+        let name = profile_data.get("name").expect("no name");
+        if let FigmentValue::String(_, s) = name {
+            assert_eq!(s, "test-name");
+        } else {
+            panic!("name should be string");
+        }
+
+        let count = profile_data.get("count").expect("no count");
+        if let FigmentValue::Num(_, figment::value::Num::U64(n)) = count {
+            assert_eq!(*n, 42);
+        } else {
+            panic!("count should be u64");
+        }
+    }
+}
diff --git a/core/configs/src/lib.rs b/core/configs/src/lib.rs
new file mode 100644
index 000000000..d1410ab8a
--- /dev/null
+++ b/core/configs/src/lib.rs
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+extern crate self as configs;
+
+mod configs_impl;
+
+pub use configs_derive::ConfigEnv;
+pub use configs_impl::{
+    ConfigEnvMappings, ConfigProvider, ConfigurationError, ConfigurationType, EnvVarMapping,
+    FileConfigProvider, TypedEnvProvider, parse_env_value_to_json,
+};
diff --git a/core/configs_derive/Cargo.toml b/core/configs_derive/Cargo.toml
new file mode 100644
index 000000000..6945a7e1d
--- /dev/null
+++ b/core/configs_derive/Cargo.toml
@@ -0,0 +1,32 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+[package]
+name = "configs_derive"
+version = "0.1.0"
+edition = "2024"
+license = "Apache-2.0"
+publish = false
+
+[lib]
+proc-macro = true
+
+[dependencies]
+darling = { workspace = true }
+proc-macro2 = { workspace = true }
+quote = { workspace = true }
+syn = { workspace = true }
diff --git a/core/configs_derive/src/config_env.rs b/core/configs_derive/src/config_env.rs
new file mode 100644
index 000000000..1eee37014
--- /dev/null
+++ b/core/configs_derive/src/config_env.rs
@@ -0,0 +1,583 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use darling::{FromDeriveInput, FromField, FromVariant};
+use proc_macro2::TokenStream as TokenStream2;
+use quote::{format_ident, quote};
+use syn::{DeriveInput, Generics, Ident, Type};
+
+/// Maximum number of array elements to generate env var mappings for.
+///
+/// For `Vec<T>` fields, mappings are generated for indices 0 through 9 (e.g.,
+/// `FIELD_0_NAME`, `FIELD_1_NAME`, ..., `FIELD_9_NAME`). Environment variables
+/// for indices beyond this limit are silently ignored.
+///
+/// This limit is sufficient for all known Iggy configuration arrays.
+const MAX_ARRAY_ELEMENTS: usize = 10;
+
+/// Container-level attributes for `#[config_env(...)]`
+#[derive(Debug, FromDeriveInput)]
+#[darling(attributes(config_env), supports(struct_named, enum_any))]
+pub struct ConfigEnvOpts {
+    ident: Ident,
+    generics: Generics,
+    data: darling::ast::Data<VariantOpts, FieldOpts>,
+
+    /// Environment variable prefix (e.g., "IGGY_"). Only needed on root config.
+    #[darling(default)]
+    prefix: Option<String>,
+
+    /// Metadata name for figment Provider (e.g., "iggy-server-config"). Only needed on root config.
+    #[darling(default)]
+    name: Option<String>,
+}
+
+/// Variant-level attributes for `#[config_env(...)]` on enums
+#[derive(Debug, FromVariant)]
+#[darling(attributes(config_env))]
+struct VariantOpts {
+    #[allow(dead_code)]
+    ident: Ident,
+    fields: darling::ast::Fields<FieldOpts>,
+
+    /// Skip this variant entirely
+    #[darling(default)]
+    skip: bool,
+}
+
+/// Field-level attributes for `#[config_env(...)]`
+#[derive(Debug, FromField)]
+#[darling(attributes(config_env))]
+struct FieldOpts {
+    ident: Option<Ident>,
+    ty: Type,
+
+    /// Explicit env var segment name (overrides field name conversion)
+    #[darling(default)]
+    name: Option<String>,
+
+    /// Skip this field entirely
+    #[darling(default)]
+    skip: bool,
+
+    /// Mark as secret (for builder documentation)
+    #[darling(default)]
+    secret: bool,
+
+    /// Treat this field as a leaf value (not a nested config struct)
+    #[darling(default)]
+    leaf: bool,
+}
+
+/// Represents a single environment variable mapping (leaf field)
+struct EnvMapping {
+    const_name: Ident,
+    /// Relative env name suffix (e.g., "ENABLED" not "IGGY_HTTP_ENABLED")
+    env_suffix: String,
+    /// Relative config path (e.g., "enabled" not "http.enabled")
+    config_path: String,
+    is_secret: bool,
+    builder_method_name: Ident,
+}
+
+/// Information about a nested field for mapping collection
+struct NestedFieldInfo {
+    /// The element type (inner type for Vec/Arc/Box/Option)
+    element_type: Type,
+    /// Whether this is a Vec field requiring array index expansion
+    is_vec: bool,
+    /// The field name in UPPER_CASE for env var construction
+    field_env_segment: String,
+    /// The field name for config path construction
+    field_name: String,
+}
+
+pub fn generate_impl(input: &DeriveInput) -> TokenStream2 {
+    match ConfigEnvOpts::from_derive_input(input) {
+        Ok(opts) => generate_from_opts(opts),
+        Err(e) => e.write_errors(),
+    }
+}
+
+fn generate_from_opts(opts: ConfigEnvOpts) -> TokenStream2 {
+    let type_name = &opts.ident;
+    let (impl_generics, ty_generics, where_clause) = opts.generics.split_for_impl();
+
+    match opts.data {
+        darling::ast::Data::Struct(fields) => generate_struct_impl(
+            type_name,
+            impl_generics,
+            ty_generics,
+            where_clause,
+            opts.prefix,
+            opts.name,
+            fields.fields,
+        ),
+        darling::ast::Data::Enum(variants) => generate_enum_impl(
+            type_name,
+            impl_generics,
+            ty_generics,
+            where_clause,
+            variants,
+        ),
+    }
+}
+
+/// Generate implementation for enum types.
+/// Combines mappings from all variant inner types that implement ConfigEnvMappings.
+fn generate_enum_impl(
+    enum_name: &Ident,
+    impl_generics: syn::ImplGenerics,
+    ty_generics: syn::TypeGenerics,
+    where_clause: Option<&syn::WhereClause>,
+    variants: Vec<VariantOpts>,
+) -> TokenStream2 {
+    // Collect inner types from newtype variants (single unnamed field)
+    let variant_types: Vec<&Type> = variants
+        .iter()
+        .filter(|v| !v.skip)
+        .filter_map(|v| {
+            // Only handle newtype variants: Variant(InnerType)
+            if v.fields.style == darling::ast::Style::Tuple && v.fields.fields.len() == 1 {
+                let field = &v.fields.fields[0];
+                if !field.skip {
+                    return Some(&field.ty);
+                }
+            }
+            None
+        })
+        .collect();
+
+    if variant_types.is_empty() {
+        return quote! {
+            impl #impl_generics configs::ConfigEnvMappings for #enum_name #ty_generics #where_clause {
+                fn env_mappings() -> &'static [configs::EnvVarMapping] {
+                    &[]
+                }
+            }
+        };
+    }
+
+    // Generate code to extend from each variant type
+    let extends: Vec<TokenStream2> = variant_types
+        .iter()
+        .map(|ty| {
+            quote! {
+                all_mappings.extend_from_slice(<#ty as configs::ConfigEnvMappings>::env_mappings());
+            }
+        })
+        .collect();
+
+    quote! {
+        impl #impl_generics configs::ConfigEnvMappings for #enum_name #ty_generics #where_clause {
+            fn env_mappings() -> &'static [configs::EnvVarMapping] {
+                static MAPPINGS: std::sync::OnceLock<Vec<configs::EnvVarMapping>> = std::sync::OnceLock::new();
+                MAPPINGS.get_or_init(|| {
+                    let mut all_mappings: Vec<configs::EnvVarMapping> = Vec::new();
+                    #(#extends)*
+                    all_mappings
+                })
+            }
+        }
+    }
+}
+
+/// Generate implementation for struct types.
+fn generate_struct_impl(
+    struct_name: &Ident,
+    impl_generics: syn::ImplGenerics,
+    ty_generics: syn::TypeGenerics,
+    where_clause: Option<&syn::WhereClause>,
+    prefix: Option<String>,
+    name: Option<String>,
+    fields: Vec<FieldOpts>,
+) -> TokenStream2 {
+    let prefix_str = prefix.as_deref().unwrap_or("");
+    let has_prefix = !prefix_str.is_empty();
+
+    // Metadata name: use provided or derive from type name (e.g., "ServerConfig" -> "server-config")
+    let metadata_name = name.unwrap_or_else(|| {
+        let type_name = struct_name.to_string();
+        let mut result = String::new();
+        for (i, c) in type_name.chars().enumerate() {
+            if c.is_uppercase() && i > 0 {
+                result.push('-');
+            }
+            result.push(c.to_ascii_lowercase());
+        }
+        result
+    });
+
+    let (mappings, nested_fields) = collect_mappings(&fields);
+
+    let const_definitions = generate_const_definitions(&mappings, prefix_str);
+    let mapping_entries = generate_mapping_entries(&mappings);
+    let builder_methods = generate_builder_methods(&mappings, struct_name, prefix_str);
+    let builder_name = format_ident!("{}EnvBuilder", struct_name);
+    let mappings_count = mappings.len();
+
+    // Generate code to include nested type mappings
+    let nested_extends: Vec<TokenStream2> = nested_fields
+        .iter()
+        .map(|info| {
+            let ty = &info.element_type;
+            let field_env_segment = &info.field_env_segment;
+            let field_name = &info.field_name;
+
+            if info.is_vec {
+                // For Vec fields, expand with array indices
+                let max_elements = MAX_ARRAY_ELEMENTS;
+                quote! {
+                    let nested_mappings = <#ty as configs::ConfigEnvMappings>::env_mappings();
+                    for i in 0..#max_elements {
+                        for mapping in nested_mappings {
+                            // Transform env_name by prepending FIELD_INDEX_
+                            let new_env_name: &'static str = Box::leak(
+                                format!("{}_{}_{}", #field_env_segment, i, mapping.env_name).into_boxed_str()
+                            );
+                            // Transform config_path by prepending field.index.
+                            let new_config_path: &'static str = Box::leak(
+                                format!("{}.{}.{}", #field_name, i, mapping.config_path).into_boxed_str()
+                            );
+                            all_mappings.push(configs::EnvVarMapping {
+                                env_name: new_env_name,
+                                config_path: new_config_path,
+                                is_secret: mapping.is_secret,
+                            });
+                        }
+                    }
+                }
+            } else {
+                // For non-Vec nested types, transform by prepending field name
+                quote! {
+                    let nested_mappings = <#ty as configs::ConfigEnvMappings>::env_mappings();
+                    for mapping in nested_mappings {
+                        // Transform env_name by prepending FIELD_
+                        let new_env_name: &'static str = Box::leak(
+                            format!("{}_{}", #field_env_segment, mapping.env_name).into_boxed_str()
+                        );
+                        // Transform config_path by prepending field.
+                        let new_config_path: &'static str = Box::leak(
+                            format!("{}.{}", #field_name, mapping.config_path).into_boxed_str()
+                        );
+                        all_mappings.push(configs::EnvVarMapping {
+                            env_name: new_env_name,
+                            config_path: new_config_path,
+                            is_secret: mapping.is_secret,
+                        });
+                    }
+                }
+            }
+        })
+        .collect();
+
+    let prefix_application = if has_prefix {
+        quote! {
+            let all_mappings: Vec<configs::EnvVarMapping> = all_mappings
+                .into_iter()
+                .map(|m| configs::EnvVarMapping {
+                    env_name: Box::leak(format!("{}{}", #prefix_str, m.env_name).into_boxed_str()),
+                    config_path: m.config_path,
+                    is_secret: m.is_secret,
+                })
+                .collect();
+        }
+    } else {
+        quote! {}
+    };
+
+    // If there are nested types, we need to use OnceLock to combine mappings
+    let env_mappings_impl = if nested_fields.is_empty() && !has_prefix {
+        // Simple case: no nested fields, no prefix transformation needed
+        quote! {
+            fn env_mappings() -> &'static [configs::EnvVarMapping] {
+                static MAPPINGS: [configs::EnvVarMapping; #mappings_count] = [
+                    #(#mapping_entries),*
+                ];
+                &MAPPINGS
+            }
+        }
+    } else {
+        // Complex case: need OnceLock for dynamic construction
+        quote! {
+            fn env_mappings() -> &'static [configs::EnvVarMapping] {
+                static MAPPINGS: std::sync::OnceLock<Vec<configs::EnvVarMapping>> = std::sync::OnceLock::new();
+                MAPPINGS.get_or_init(|| {
+                    let own_mappings: [configs::EnvVarMapping; #mappings_count] = [
+                        #(#mapping_entries),*
+                    ];
+                    let mut all_mappings = Vec::from(own_mappings);
+                    #(#nested_extends)*
+                    #prefix_application
+                    all_mappings
+                })
+            }
+        }
+    };
+
+    quote! {
+        impl #impl_generics #struct_name #ty_generics #where_clause {
+            /// Environment variable prefix for this config type.
+            pub const ENV_PREFIX: &'static str = #prefix_str;
+
+            /// Metadata name for figment Provider.
+            pub const ENV_PROVIDER_NAME: &'static str = #metadata_name;
+
+            #(#const_definitions)*
+        }
+
+        impl #impl_generics configs::ConfigEnvMappings for #struct_name #ty_generics #where_clause {
+            #env_mappings_impl
+        }
+
+        /// Type-safe builder for constructing environment variable maps for tests.
+        #[derive(Debug, Default, Clone)]
+        pub struct #builder_name {
+            envs: std::collections::HashMap<String, String>,
+        }
+
+        impl #builder_name {
+            /// Create a new builder instance.
+            pub fn new() -> Self {
+                Self::default()
+            }
+
+            #(#builder_methods)*
+
+            /// Set a custom environment variable.
+            pub fn custom(mut self, key: impl Into<String>, value: impl std::fmt::Display) -> Self {
+                self.envs.insert(key.into(), value.to_string());
+                self
+            }
+
+            /// Extend with another builder's values.
+            pub fn extend(mut self, other: Self) -> Self {
+                self.envs.extend(other.envs);
+                self
+            }
+
+            /// Build the final environment variable map.
+            pub fn build(self) -> std::collections::HashMap<String, String> {
+                self.envs
+            }
+
+            /// Build and merge with existing environment variables.
+            pub fn build_with(self, existing: std::collections::HashMap<String, String>) -> std::collections::HashMap<String, String> {
+                let mut result = existing;
+                result.extend(self.envs);
+                result
+            }
+        }
+    }
+}
+
+/// Check if a type is a Rust primitive that doesn't need nested expansion.
+fn is_primitive_type(ty: &Type) -> bool {
+    let Type::Path(type_path) = ty else {
+        return false;
+    };
+    let Some(segment) = type_path.path.segments.last() else {
+        return false;
+    };
+
+    let ident = segment.ident.to_string();
+
+    // Rust primitives only
+    if matches!(
+        ident.as_str(),
+        "bool"
+            | "u8"
+            | "u16"
+            | "u32"
+            | "u64"
+            | "u128"
+            | "i8"
+            | "i16"
+            | "i32"
+            | "i64"
+            | "i128"
+            | "f32"
+            | "f64"
+            | "usize"
+            | "isize"
+            | "String"
+            | "str"
+    ) {
+        return true;
+    }
+
+    // Option<T> - check inner type
+    if ident == "Option"
+        && let syn::PathArguments::AngleBracketed(args) = &segment.arguments
+        && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
+    {
+        return is_primitive_type(inner);
+    }
+
+    false
+}
+
+/// Extract type info, unwrapping Vec/Arc/Box/Option.
+/// Returns (inner_type, is_vec).
+fn extract_type_info(ty: &Type) -> (Type, bool) {
+    let Type::Path(type_path) = ty else {
+        return (ty.clone(), false);
+    };
+    let Some(segment) = type_path.path.segments.last() else {
+        return (ty.clone(), false);
+    };
+
+    let ident = segment.ident.to_string();
+    match ident.as_str() {
+        "Vec" => {
+            if let syn::PathArguments::AngleBracketed(args) = &segment.arguments
+                && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
+            {
+                let (inner_type, _) = extract_type_info(inner);
+                return (inner_type, true);
+            }
+        }
+        "Arc" | "Box" | "Rc" | "Option" => {
+            if let syn::PathArguments::AngleBracketed(args) = &segment.arguments
+                && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
+            {
+                return extract_type_info(inner);
+            }
+        }
+        _ => {}
+    }
+    (ty.clone(), false)
+}
+
+fn collect_mappings(fields: &[FieldOpts]) -> (Vec<EnvMapping>, Vec<NestedFieldInfo>) {
+    let mut mappings = Vec::new();
+    let mut nested_fields = Vec::new();
+
+    for field in fields {
+        if field.skip {
+            continue;
+        }
+
+        let field_ident = match &field.ident {
+            Some(ident) => ident,
+            None => continue,
+        };
+
+        let field_name = field_ident.to_string();
+        let field_env_segment = field_name.to_uppercase();
+
+        // Check if this is a leaf type or needs nested handling
+        let (inner_type, is_vec) = extract_type_info(&field.ty);
+
+        // A field is a leaf if:
+        // 1. It's explicitly marked with #[config_env(leaf)]
+        // 2. It's a Rust primitive type (bool, u32, String, etc.)
+        let is_leaf = field.leaf || is_primitive_type(&inner_type);
+
+        if is_leaf {
+            // Leaf field: generate a direct mapping with relative paths
+            let env_suffix = field
+                .name
+                .clone()
+                .unwrap_or_else(|| field_env_segment.clone());
+
+            let const_name = format_ident!("ENV_VAR_{}", field_env_segment);
+            let builder_method_name = format_ident!("{}", field_name);
+
+            mappings.push(EnvMapping {
+                const_name,
+                env_suffix,
+                config_path: field_name,
+                is_secret: field.secret,
+                builder_method_name,
+            });
+        } else {
+            // Nested field: will include mappings from the nested type
+            nested_fields.push(NestedFieldInfo {
+                element_type: inner_type,
+                is_vec,
+                field_env_segment,
+                field_name,
+            });
+        }
+    }
+
+    (mappings, nested_fields)
+}
+
+fn generate_const_definitions(mappings: &[EnvMapping], prefix: &str) -> Vec<TokenStream2> {
+    mappings
+        .iter()
+        .map(|m| {
+            let const_name = &m.const_name;
+            let env_var_name = format!("{}{}", prefix, m.env_suffix);
+            quote! {
+                pub const #const_name: &'static str = #env_var_name;
+            }
+        })
+        .collect()
+}
+
+fn generate_mapping_entries(mappings: &[EnvMapping]) -> Vec<TokenStream2> {
+    mappings
+        .iter()
+        .map(|m| {
+            let env_suffix = &m.env_suffix;
+            let config_path = &m.config_path;
+            let is_secret = m.is_secret;
+
+            quote! {
+                configs::EnvVarMapping {
+                    env_name: #env_suffix,
+                    config_path: #config_path,
+                    is_secret: #is_secret,
+                }
+            }
+        })
+        .collect()
+}
+
+fn generate_builder_methods(
+    mappings: &[EnvMapping],
+    struct_name: &Ident,
+    prefix: &str,
+) -> Vec<TokenStream2> {
+    mappings
+        .iter()
+        .map(|m| {
+            let method_name = &m.builder_method_name;
+            let const_name = &m.const_name;
+            let env_var_name = format!("{}{}", prefix, m.env_suffix);
+            let doc_secret = if m.is_secret {
+                " (secret - will be masked in logs)"
+            } else {
+                ""
+            };
+            let doc = format!("Set `{}` environment variable.{}", env_var_name, doc_secret);
+
+            quote! {
+                #[doc = #doc]
+                pub fn #method_name(mut self, value: impl std::fmt::Display) -> Self {
+                    self.envs.insert(#struct_name::#const_name.into(), value.to_string());
+                    self
+                }
+            }
+        })
+        .collect()
+}
diff --git a/core/configs_derive/src/lib.rs b/core/configs_derive/src/lib.rs
new file mode 100644
index 000000000..7435e42cb
--- /dev/null
+++ b/core/configs_derive/src/lib.rs
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+//! Proc macro for generating type-safe environment variable mappings for configuration structs.
+//!
+//! This crate provides `#[derive(ConfigEnv)]` which generates:
+//! - Compile-time constants for all valid env var names
+//! - Type-safe builder API for tests
+//! - `ConfigEnvMappings` trait implementation for runtime discovery
+//!
+//! # Path Deduction
+//!
+//! The macro automatically deduces config paths from field names:
+//! - Each struct generates **relative** mappings (just its own field names)
+//! - Parents transform nested mappings by prepending the field name
+//! - Only the root struct needs `prefix`
+//!
+//! # Type Inference
+//!
+//! - **Leaf types** (primitives, String, known value types): generate direct mappings
+//! - **Nested types** (structs with `ConfigEnv` derive): include their mappings recursively
+//! - **Vec<T>**: expands to indexed mappings (e.g., `FIELD_0_NAME`, `FIELD_1_NAME`, ...)
+//! - **Arc<T>**, **Box<T>**, **Option<T>**: transparently unwrapped
+
+mod config_env;
+
+use proc_macro::TokenStream;
+use syn::{DeriveInput, parse_macro_input};
+
+/// Derive macro for generating environment variable mappings.
+///
+/// # Container Attributes
+/// - `#[config_env(prefix = "IGGY_")]` - Sets the env var prefix (only on root config)
+/// - `#[config_env(name = "iggy-server-config")]` - Sets the figment Provider metadata name
+///
+/// Root configs should specify both `prefix` and `name`:
+/// ```ignore
+/// #[derive(ConfigEnv)]
+/// #[config_env(prefix = "IGGY_", name = "iggy-server-config")]
+/// pub struct ServerConfig { ... }
+/// ```
+///
+/// This generates constants `ServerConfig::ENV_PREFIX` and `ServerConfig::ENV_PROVIDER_NAME`.
+///
+/// # Field Attributes
+/// - `#[config_env(name = "CUSTOM")]` - Override the env var segment name
+/// - `#[config_env(skip)]` - Exclude this field from env var mapping
+/// - `#[config_env(secret)]` - Mark field as secret (masked in logs)
+/// - `#[config_env(leaf)]` - Treat as value type, not nested config (for custom types like enums)
+///
+/// # Type Handling
+/// - **Primitives** (bool, u8-u128, i8-i128, f32, f64, String): automatically treated as leaves
+/// - **Non-primitives**: assumed to be nested configs that implement `ConfigEnvMappings`
+/// - Use `#[config_env(leaf)]` for value types that shouldn't be recursively expanded
+///   (e.g., `IggyDuration`, `CompressionAlgorithm`, custom enums)
+///
+/// # Examples
+///
+/// ## Struct Configuration
+/// ```ignore
+/// // Root config with prefix and name
+/// #[derive(ConfigEnv)]
+/// #[config_env(prefix = "IGGY_", name = "iggy-server-config")]
+/// pub struct ServerConfig {
+///     pub http: HttpConfig,              // Nested: IGGY_HTTP_*
+///     #[config_env(leaf)]
+///     pub compression: CompressionAlgorithm,  // Leaf: IGGY_COMPRESSION
+/// }
+///
+/// // Nested config - no prefix needed, paths deduced from field names
+/// #[derive(ConfigEnv)]
+/// pub struct HttpConfig {
+///     pub enabled: bool,       // ENABLED -> parent adds HTTP_ -> IGGY_HTTP_ENABLED
+///     pub port: u16,           // PORT -> IGGY_HTTP_PORT
+///     #[config_env(leaf)]
+///     pub timeout: IggyDuration,  // Leaf: TIMEOUT -> IGGY_HTTP_TIMEOUT
+/// }
+/// ```
+///
+/// ## Enum Configuration
+/// ```ignore
+/// // Enums with newtype variants collect mappings from inner types
+/// #[derive(ConfigEnv)]
+/// #[serde(tag = "type", rename_all = "lowercase")]
+/// pub enum ConnectorConfig {
+///     Local(LocalConfig),   // Includes LocalConfig's mappings
+///     Http(HttpConfig),     // Includes HttpConfig's mappings
+/// }
+///
+/// // Simple value enums should NOT derive ConfigEnv - mark the field as leaf instead
+/// #[derive(Serialize, Deserialize)]
+/// pub enum Transport { Tcp, Quic, Http }
+///
+/// #[derive(ConfigEnv)]
+/// pub struct ServerConfig {
+///     #[config_env(leaf)]  // Transport is a simple enum, not a nested config
+///     pub transport: Transport,
+/// }
+/// ```
+#[proc_macro_derive(ConfigEnv, attributes(config_env))]
+pub fn derive_config_env(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as DeriveInput);
+    config_env::generate_impl(&input).into()
+}
diff --git a/core/connectors/runtime/Cargo.toml b/core/connectors/runtime/Cargo.toml
index dea88451a..970c1a7e8 100644
--- a/core/connectors/runtime/Cargo.toml
+++ b/core/connectors/runtime/Cargo.toml
@@ -34,6 +34,8 @@ axum = { workspace = true }
 axum-server = { workspace = true }
 chrono = { workspace = true }
 clap = { workspace = true }
+configs = { workspace = true }
+configs_derive = { workspace = true }
 dashmap = { workspace = true }
 derive_more = { workspace = true }
 dirs = { workspace = true }
diff --git a/core/connectors/runtime/src/api/config.rs b/core/connectors/runtime/src/api/config.rs
index 1e11791b8..8efdb8ae5 100644
--- a/core/connectors/runtime/src/api/config.rs
+++ b/core/connectors/runtime/src/api/config.rs
@@ -20,6 +20,7 @@
 use crate::configs::connectors::ConfigFormat;
 use crate::error::RuntimeError;
 use axum::http::{HeaderValue, Method};
+use configs_derive::ConfigEnv;
 use serde::{Deserialize, Serialize};
 use std::fmt::Formatter;
 use tower_http::cors::{AllowOrigin, CorsLayer};
@@ -30,16 +31,17 @@ pub const YAML_HEADER: HeaderValue = HeaderValue::from_static("application/yaml"
 pub const TOML_HEADER: HeaderValue = HeaderValue::from_static("application/toml");
 pub const TEXT_HEADER: HeaderValue = HeaderValue::from_static("text/plain");
 
-#[derive(Debug, Clone, Deserialize, Serialize)]
+#[derive(Debug, Clone, Deserialize, Serialize, ConfigEnv)]
 pub struct HttpConfig {
     pub enabled: bool,
     pub address: String,
+    #[config_env(secret)]
     pub api_key: String,
     pub cors: HttpCorsConfig,
     pub tls: HttpTlsConfig,
 }
 
-#[derive(Debug, Default, Deserialize, Serialize, Clone)]
+#[derive(Debug, Default, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct HttpCorsConfig {
     pub enabled: bool,
     pub allowed_methods: Vec<String>,
@@ -50,7 +52,7 @@ pub struct HttpCorsConfig {
     pub allow_private_network: bool,
 }
 
-#[derive(Debug, Default, Deserialize, Serialize, Clone)]
+#[derive(Debug, Default, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct HttpTlsConfig {
     pub enabled: bool,
     pub cert_file: String,
diff --git a/core/connectors/runtime/src/configs/connectors.rs b/core/connectors/runtime/src/configs/connectors.rs
index 14dd0bdf8..3c111cfaa 100644
--- a/core/connectors/runtime/src/configs/connectors.rs
+++ b/core/connectors/runtime/src/configs/connectors.rs
@@ -26,6 +26,7 @@ use crate::configs::runtime::ConnectorsConfig as RuntimeConnectorsConfig;
 use crate::error::RuntimeError;
 use async_trait::async_trait;
 use chrono::{DateTime, Utc};
+use configs_derive::ConfigEnv;
 use iggy_connector_sdk::Schema;
 use iggy_connector_sdk::transforms::TransformType;
 use serde::{Deserialize, Serialize};
@@ -101,16 +102,19 @@ impl CreateSinkConfig {
     }
 }
 
-#[derive(Debug, Default, Clone, Serialize, Deserialize)]
+#[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigEnv)]
 pub struct SinkConfig {
     pub key: String,
     pub enabled: bool,
     pub version: u64,
     pub name: String,
     pub path: String,
+    #[config_env(skip)]
     pub transforms: Option<TransformsConfig>,
     pub streams: Vec<StreamConsumerConfig>,
+    #[config_env(leaf)]
     pub plugin_config_format: Option<ConfigFormat>,
+    #[config_env(skip)]
     pub plugin_config: Option<serde_json::Value>,
     #[serde(default)]
     pub verbose: bool,
@@ -146,16 +150,19 @@ impl CreateSourceConfig {
     }
 }
 
-#[derive(Debug, Default, Clone, Serialize, Deserialize)]
+#[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigEnv)]
 pub struct SourceConfig {
     pub key: String,
     pub enabled: bool,
     pub version: u64,
     pub name: String,
     pub path: String,
+    #[config_env(skip)]
     pub transforms: Option<TransformsConfig>,
     pub streams: Vec<StreamProducerConfig>,
+    #[config_env(leaf)]
     pub plugin_config_format: Option<ConfigFormat>,
+    #[config_env(skip)]
     pub plugin_config: Option<serde_json::Value>,
     #[serde(default)]
     pub verbose: bool,
@@ -167,20 +174,22 @@ pub struct TransformsConfig {
     pub transforms: HashMap<TransformType, serde_json::Value>,
 }
 
-#[derive(Debug, Default, Clone, Serialize, Deserialize)]
+#[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigEnv)]
 pub struct StreamConsumerConfig {
     pub stream: String,
     pub topics: Vec<String>,
+    #[config_env(leaf)]
     pub schema: Schema,
     pub batch_length: Option<u32>,
     pub poll_interval: Option<String>,
     pub consumer_group: Option<String>,
 }
 
-#[derive(Debug, Default, Clone, Serialize, Deserialize)]
+#[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigEnv)]
 pub struct StreamProducerConfig {
     pub stream: String,
     pub topic: String,
+    #[config_env(leaf)]
     pub schema: Schema,
     pub batch_length: Option<u32>,
     pub linger_time: Option<String>,
diff --git a/core/connectors/runtime/src/configs/connectors/local_provider.rs b/core/connectors/runtime/src/configs/connectors/local_provider.rs
index fb363d9c5..6d1d3b45d 100644
--- a/core/connectors/runtime/src/configs/connectors/local_provider.rs
+++ b/core/connectors/runtime/src/configs/connectors/local_provider.rs
@@ -22,12 +22,12 @@ use crate::configs::connectors::{
     ConnectorsConfigProvider, CreateSinkConfig, CreateSourceConfig, SinkConfig, SourceConfig,
 };
 use crate::error::RuntimeError;
+use ::configs::{ConfigProvider, FileConfigProvider, TypedEnvProvider};
 use async_trait::async_trait;
 use chrono::{DateTime, Utc};
 use dashmap::DashMap;
 use figment::value::Dict;
 use figment::{Metadata, Profile, Provider};
-use iggy_common::{ConfigProvider, CustomEnvProvider, FileConfigProvider};
 use serde::{Deserialize, Serialize};
 use std::collections::HashMap;
 use std::path::Path;
@@ -338,7 +338,7 @@ impl<S: ProviderState> LocalConnectorsConfigProvider<S> {
 
             let field_path = &env_key_upper[prefix.len()..];
             let field_name = field_path.to_lowercase();
-            let parsed_value = iggy_common::parse_env_value_to_json(&env_value);
+            let parsed_value = ::configs::parse_env_value_to_json(&env_value);
 
             let config = plugin_config.get_or_insert_with(|| serde_json::json!({}));
             if let serde_json::Value::Object(map) = config {
@@ -783,9 +783,15 @@ impl ConnectorsConfigProvider for LocalConnectorsConfigProvider<Initialized> {
 }
 
 #[derive(Debug, Clone)]
-pub struct ConnectorEnvProvider {
-    connector_name: String,
-    provider: CustomEnvProvider<ConnectorConfig>,
+pub enum ConnectorEnvProvider {
+    Sink {
+        connector_name: String,
+        provider: TypedEnvProvider<SinkConfig>,
+    },
+    Source {
+        connector_name: String,
+        provider: TypedEnvProvider<SourceConfig>,
+    },
 }
 
 impl ConnectorEnvProvider {
@@ -793,24 +799,38 @@ impl ConnectorEnvProvider {
         let connector_type = base_config.connector_type().to_uppercase();
         let key = base_config.key().to_uppercase();
         let prefix = format!("IGGY_CONNECTORS_{}_{}_", connector_type, key);
-        Self {
-            connector_name: base_config.key().to_owned(),
-            provider: CustomEnvProvider::new(&prefix, &[]),
+        let connector_name = base_config.key().to_owned();
+
+        match base_config {
+            BaseConnectorConfig::Sink { .. } => Self::Sink {
+                connector_name,
+                provider: TypedEnvProvider::with_runtime_prefix(&prefix, &[]),
+            },
+            BaseConnectorConfig::Source { .. } => Self::Source {
+                connector_name,
+                provider: TypedEnvProvider::with_runtime_prefix(&prefix, &[]),
+            },
         }
     }
 }
 
 impl Provider for ConnectorEnvProvider {
     fn metadata(&self) -> Metadata {
-        Metadata::named(format!("iggy-connectors-{}-config", self.connector_name))
+        let name = match self {
+            Self::Sink { connector_name, .. } => connector_name,
+            Self::Source { connector_name, .. } => connector_name,
+        };
+        Metadata::named(format!("iggy-connectors-{}-config", name))
     }
 
     fn data(&self) -> Result<figment::value::Map<Profile, Dict>, figment::Error> {
-        self.provider.deserialize().map_err(|_| {
-            figment::Error::from(format!(
-                "Cannot deserialize environment variables for connector config {}",
-                self.connector_name
-            ))
-        })
+        match self {
+            Self::Sink { provider, .. } => provider
+                .deserialize_with_runtime_prefix()
+                .map_err(|e| figment::Error::from(format!("Failed to deserialize env vars: {e}"))),
+            Self::Source { provider, .. } => provider
+                .deserialize_with_runtime_prefix()
+                .map_err(|e| figment::Error::from(format!("Failed to deserialize env vars: {e}"))),
+        }
     }
 }
diff --git a/core/connectors/runtime/src/configs/runtime.rs b/core/connectors/runtime/src/configs/runtime.rs
index 77d88ef07..edcf9fcca 100644
--- a/core/connectors/runtime/src/configs/runtime.rs
+++ b/core/connectors/runtime/src/configs/runtime.rs
@@ -18,24 +18,21 @@
  */
 
 use crate::api::config::HttpConfig;
+use ::configs::{FileConfigProvider, TypedEnvProvider};
+use configs_derive::ConfigEnv;
 use derive_more::Display;
 use figment::providers::{Format, Toml};
 use figment::value::Dict;
 use figment::{Metadata, Profile, Provider};
+use iggy_common::IggyDuration;
 use iggy_common::defaults::{DEFAULT_ROOT_PASSWORD, DEFAULT_ROOT_USERNAME};
-use iggy_common::{CustomEnvProvider, FileConfigProvider, IggyDuration};
 use serde::{Deserialize, Serialize};
 use serde_with::{DisplayFromStr, serde_as};
 use std::collections::HashMap;
 use std::fmt::{Display, Formatter};
 use std::str::FromStr;
 
-const SECRET_KEYS: [&str; 2] = [
-    "IGGY_CONNECTORS_IGGY_PASSWORD",
-    "IGGY_CONNECTORS_IGGY_TOKEN",
-];
-
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct TelemetryConfig {
     pub enabled: bool,
     pub service_name: String,
@@ -64,8 +61,9 @@ impl Display for TelemetryConfig {
     }
 }
 
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct TelemetryLogsConfig {
+    #[config_env(leaf)]
     pub transport: TelemetryTransport,
     pub endpoint: String,
 }
@@ -89,8 +87,9 @@ impl Display for TelemetryLogsConfig {
     }
 }
 
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct TelemetryTracesConfig {
+    #[config_env(leaf)]
     pub transport: TelemetryTransport,
     pub endpoint: String,
 }
@@ -134,7 +133,8 @@ impl FromStr for TelemetryTransport {
     }
 }
 
-#[derive(Debug, Default, Clone, Deserialize, Serialize)]
+#[derive(Debug, Default, Clone, Deserialize, Serialize, ConfigEnv)]
+#[config_env(prefix = "IGGY_CONNECTORS_", name = "iggy-connectors-config")]
 #[serde(default)]
 pub struct ConnectorsRuntimeConfig {
     pub http: HttpConfig,
@@ -144,16 +144,18 @@ pub struct ConnectorsRuntimeConfig {
     pub telemetry: TelemetryConfig,
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, ConfigEnv)]
 pub struct IggyConfig {
     pub address: String,
     pub username: String,
+    #[config_env(secret)]
     pub password: String,
+    #[config_env(secret)]
     pub token: String,
     pub tls: IggyTlsConfig,
 }
 
-#[derive(Debug, Default, Clone, Serialize, Deserialize)]
+#[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigEnv)]
 pub struct IggyTlsConfig {
     pub enabled: bool,
     pub ca_file: String,
@@ -161,12 +163,14 @@ pub struct IggyTlsConfig {
 }
 
 #[serde_as]
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, ConfigEnv)]
 pub struct RetryConfig {
     pub enabled: bool,
     pub max_attempts: u32,
+    #[config_env(leaf)]
     #[serde_as(as = "DisplayFromStr")]
     pub initial_backoff: IggyDuration,
+    #[config_env(leaf)]
     #[serde_as(as = "DisplayFromStr")]
     pub max_backoff: IggyDuration,
     pub backoff_multiplier: u32,
@@ -198,21 +202,24 @@ impl Display for RetryConfig {
     }
 }
 
-#[derive(Debug, Default, Clone, Deserialize, Serialize)]
+#[derive(Debug, Default, Clone, Deserialize, Serialize, ConfigEnv)]
 #[serde(default)]
 pub struct LocalConnectorsConfig {
     pub config_dir: String,
 }
 
 #[serde_as]
-#[derive(Debug, Default, Clone, Deserialize, Serialize)]
+#[derive(Debug, Default, Clone, Deserialize, Serialize, ConfigEnv)]
 pub struct HttpConnectorsConfig {
     pub base_url: String,
+    #[config_env(leaf)]
     #[serde_as(as = "DisplayFromStr")]
     #[serde(default = "default_from_secs")]
     pub timeout: IggyDuration,
+    #[config_env(skip)]
     #[serde(default)]
     pub request_headers: HashMap<String, String>,
+    #[config_env(skip)]
     #[serde(default)]
     pub url_templates: HashMap<String, String>,
     #[serde(default)]
@@ -240,7 +247,7 @@ fn default_from_secs() -> IggyDuration {
     IggyDuration::new_from_secs(10)
 }
 
-#[derive(Debug, Default, Clone, Deserialize, Serialize)]
+#[derive(Debug, Default, Clone, Deserialize, Serialize, ConfigEnv)]
 #[serde(default)]
 pub struct ResponseConfig {
     pub data_path: Option<String>,
@@ -248,7 +255,7 @@ pub struct ResponseConfig {
 }
 
 #[allow(clippy::large_enum_variant)]
-#[derive(Debug, Clone, Deserialize, Serialize)]
+#[derive(Debug, Clone, Deserialize, Serialize, ConfigEnv)]
 #[serde(tag = "config_type", rename_all = "lowercase")]
 pub enum ConnectorsConfig {
     Local(LocalConnectorsConfig),
@@ -261,7 +268,7 @@ impl Default for ConnectorsConfig {
     }
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, ConfigEnv)]
 pub struct StateConfig {
     pub path: String,
 }
@@ -358,25 +365,23 @@ impl ConnectorsRuntimeConfig {
 
 #[derive(Debug, Clone)]
 pub struct ConnectorsEnvProvider {
-    provider: CustomEnvProvider<ConnectorsRuntimeConfig>,
+    provider: TypedEnvProvider<ConnectorsRuntimeConfig>,
 }
 
 impl Default for ConnectorsEnvProvider {
     fn default() -> Self {
         Self {
-            provider: CustomEnvProvider::new("IGGY_CONNECTORS_", &SECRET_KEYS),
+            provider: TypedEnvProvider::from_config(ConnectorsRuntimeConfig::ENV_PREFIX),
         }
     }
 }
 
 impl Provider for ConnectorsEnvProvider {
     fn metadata(&self) -> Metadata {
-        Metadata::named("iggy-connectors-config")
+        Metadata::named(ConnectorsRuntimeConfig::ENV_PROVIDER_NAME)
     }
 
     fn data(&self) -> Result<figment::value::Map<Profile, Dict>, figment::Error> {
-        self.provider.deserialize().map_err(|_| {
-            figment::Error::from("Cannot deserialize environment variables for connectors config")
-        })
+        self.provider.data()
     }
 }
diff --git a/core/connectors/runtime/src/main.rs b/core/connectors/runtime/src/main.rs
index ab4a77208..657ed646a 100644
--- a/core/connectors/runtime/src/main.rs
+++ b/core/connectors/runtime/src/main.rs
@@ -18,6 +18,7 @@
  */
 
 use crate::configs::connectors::{ConnectorsConfigProvider, create_connectors_config_provider};
+use ::configs::ConfigProvider;
 use clap::Parser;
 use configs::connectors::ConfigFormat;
 use configs::runtime::ConnectorsRuntimeConfig;
@@ -26,7 +27,6 @@ use dotenvy::dotenv;
 use error::RuntimeError;
 use figlet_rs::FIGfont;
 use iggy::prelude::{Client, IggyConsumer, IggyProducer};
-use iggy_common::ConfigProvider;
 use iggy_connector_sdk::{
     StreamDecoder, StreamEncoder,
     sink::ConsumeCallback,
diff --git a/core/integration/Cargo.toml b/core/integration/Cargo.toml
index 87b95b6f1..3e9cb5376 100644
--- a/core/integration/Cargo.toml
+++ b/core/integration/Cargo.toml
@@ -33,6 +33,7 @@ async-trait = { workspace = true }
 bytes = { workspace = true }
 chrono = { workspace = true }
 compio = { workspace = true }
+configs = { workspace = true }
 ctor = "0.6.3"
 derive_more = { workspace = true }
 env_logger = { workspace = true }
diff --git a/core/integration/src/test_connectors_runtime.rs b/core/integration/src/test_connectors_runtime.rs
index 995feb69f..b7adb73ef 100644
--- a/core/integration/src/test_connectors_runtime.rs
+++ b/core/integration/src/test_connectors_runtime.rs
@@ -31,7 +31,6 @@ use uuid::Uuid;
 
 pub const STATE_PATH_ENV_VAR: &str = "IGGY_CONNECTORS_STATE_PATH";
 pub const TEST_VERBOSITY_ENV_VAR: &str = "IGGY_TEST_VERBOSE";
-pub const CONSUMER_NAME: &str = "connectors";
 const LOCAL_STATE_PREFIX: &str = "local_state_";
 
 #[derive(Debug)]
@@ -70,10 +69,6 @@ impl TestConnectorsRuntime {
             "IGGY_CONNECTORS_IGGY_ADDRESS".to_string(),
             iggy_tcp_server_address.to_string(),
         );
-        envs.insert(
-            "IGGY_CONNECTORS_IGGY_CONSUMER".to_string(),
-            CONSUMER_NAME.to_string(),
-        );
         Self::create(envs, server_executable_path)
     }
 
diff --git a/core/integration/src/test_server.rs b/core/integration/src/test_server.rs
index 99846d4ad..a58ad85a3 100644
--- a/core/integration/src/test_server.rs
+++ b/core/integration/src/test_server.rs
@@ -19,11 +19,12 @@
 
 use assert_cmd::prelude::CommandCargoExt;
 use async_trait::async_trait;
+use configs::ConfigProvider;
 use derive_more::Display;
 use futures::executor::block_on;
 use iggy::prelude::UserStatus::Active;
 use iggy::prelude::*;
-use iggy_common::{ConfigProvider, TransportProtocol};
+use iggy_common::TransportProtocol;
 use rand::Rng;
 use server::configs::server::ServerConfig;
 use std::collections::HashMap;
diff --git a/core/integration/tests/config_provider/mod.rs b/core/integration/tests/config_provider/mod.rs
index 82e039732..a705284f4 100644
--- a/core/integration/tests/config_provider/mod.rs
+++ b/core/integration/tests/config_provider/mod.rs
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-use iggy_common::ConfigProvider;
+use configs::ConfigProvider;
 use integration::file::get_root_path;
 use serial_test::serial;
 use server::configs::server::ServerConfig;
@@ -44,13 +44,13 @@ async fn validate_config_env_override() {
         );
     }
 
-    let config_path = get_root_path().join("../configs/server.toml");
+    let config_path = get_root_path().join("../server/config.toml");
     let file_config_provider =
         ServerConfig::config_provider(&config_path.as_path().display().to_string());
     let config: ServerConfig = file_config_provider
         .load_config()
         .await
-        .expect("Failed to load server.toml config");
+        .expect("Failed to load config.toml config");
 
     assert_eq!(config.http.enabled, expected_http);
     assert_eq!(config.tcp.enabled, expected_tcp);
@@ -86,13 +86,13 @@ async fn validate_socket_override() {
         );
     }
 
-    let config_path = get_root_path().join("../configs/server.toml");
+    let config_path = get_root_path().join("../server/config.toml");
     let file_config_provider =
         ServerConfig::config_provider(&config_path.as_path().display().to_string());
     let config: ServerConfig = file_config_provider
         .load_config()
         .await
-        .expect("Failed to load server.toml config with socket override");
+        .expect("Failed to load config.toml config with socket override");
 
     assert!(config.tcp.socket.override_defaults);
     // Verify the buffer sizes match the expected byte counts
@@ -115,13 +115,13 @@ async fn validate_socket_override() {
 #[serial]
 #[tokio::test]
 async fn validate_socket_no_override() {
-    let config_path = get_root_path().join("../configs/server.toml");
+    let config_path = get_root_path().join("../server/config.toml");
     let file_config_provider =
         ServerConfig::config_provider(&config_path.as_path().display().to_string());
     let config: ServerConfig = file_config_provider
         .load_config()
         .await
-        .expect("Failed to load server.toml config without socket override");
+        .expect("Failed to load config.toml config without socket override");
 
     assert!(!config.tcp.socket.override_defaults);
 }
@@ -201,13 +201,13 @@ async fn validate_cluster_config_env_override() {
         );
     }
 
-    let config_path = get_root_path().join("../configs/server.toml");
+    let config_path = get_root_path().join("../server/config.toml");
     let file_config_provider =
         ServerConfig::config_provider(&config_path.as_path().display().to_string());
     let config: ServerConfig = file_config_provider
         .load_config()
         .await
-        .expect("Failed to load server.toml config with cluster env overrides");
+        .expect("Failed to load config.toml config with cluster env overrides");
 
     // Verify cluster configuration
     assert_eq!(config.cluster.enabled, expected_cluster_enabled);
@@ -270,7 +270,6 @@ async fn validate_cluster_config_env_override() {
     unsafe {
         // Clean up environment variables
         env::remove_var("IGGY_CLUSTER_ENABLED");
-        env::remove_var("IGGY_CLUSTER_ID");
         env::remove_var("IGGY_CLUSTER_NAME");
         env::remove_var("IGGY_CLUSTER_NODE_CURRENT_NAME");
 
@@ -392,13 +391,13 @@ async fn validate_four_node_cluster_config_env_override() {
         // IGGY_CLUSTER_NODE_OTHERS_2_PORTS_WEBSOCKET is NOT set - should use default (8092)
     }
 
-    let config_path = get_root_path().join("../configs/server.toml");
+    let config_path = get_root_path().join("../server/config.toml");
     let file_config_provider =
         ServerConfig::config_provider(&config_path.as_path().display().to_string());
     let config: ServerConfig = file_config_provider
         .load_config()
         .await
-        .expect("Failed to load server.toml config with 4-node cluster env overrides");
+        .expect("Failed to load config.toml config with 4-node cluster env overrides");
 
     // Verify cluster configuration
     assert_eq!(config.cluster.enabled, expected_cluster_enabled);
diff --git a/core/integration/tests/data_integrity/verify_after_server_restart.rs b/core/integration/tests/data_integrity/verify_after_server_restart.rs
index ac63d6e71..655fc8246 100644
--- a/core/integration/tests/data_integrity/verify_after_server_restart.rs
+++ b/core/integration/tests/data_integrity/verify_after_server_restart.rs
@@ -52,7 +52,7 @@ async fn should_fill_data_and_verify_after_restart(cache_setting: &'static str)
             TestServer::get_random_path(),
         ),
         (
-            "IGGY_SEGMENT_CACHE_INDEXES".to_string(),
+            "IGGY_SYSTEM_SEGMENT_CACHE_INDEXES".to_string(),
             cache_setting.to_string(),
         ),
     ]);
diff --git a/core/server/Cargo.toml b/core/server/Cargo.toml
index 3e4e7192f..d311bc870 100644
--- a/core/server/Cargo.toml
+++ b/core/server/Cargo.toml
@@ -53,6 +53,7 @@ compio-net = { workspace = true }
 compio-quic = { workspace = true }
 compio-tls = { workspace = true }
 compio-ws = { workspace = true }
+configs = { workspace = true }
 ctrlc = { version = "3.5", features = ["termination"] }
 cyper = { workspace = true }
 cyper-axum = { workspace = true }
diff --git a/core/server/README.md b/core/server/README.md
index 5608b4e0d..a57c62401 100644
--- a/core/server/README.md
+++ b/core/server/README.md
@@ -2,7 +2,7 @@
 
 This is the core server component of Apache Iggy. You can run it directly with `cargo run --bin iggy-server --release` or use the Docker image `apache/iggy:latest` (the `edge` tag is for the latest development version).
 
-The configuration file is located at [core/configs/server.toml](https://github.com/apache/iggy/blob/master/core/configs/server.toml). You can customize the server settings by modifying this file or by using environment variables e.g. `IGGY_TCP_ADDRESS=0.0.0.0:8090`.
+The configuration file is located at [core/server/config.toml](https://github.com/apache/iggy/blob/master/core/server/config.toml). You can customize the server settings by modifying this file or by using environment variables e.g. `IGGY_TCP_ADDRESS=0.0.0.0:8090`.
 
 ![Server](../../assets/server.png)
 
diff --git a/core/configs/server.toml b/core/server/config.toml
similarity index 100%
rename from core/configs/server.toml
rename to core/server/config.toml
diff --git a/core/server/src/args.rs b/core/server/src/args.rs
index 8ec1dc09d..13451f411 100644
--- a/core/server/src/args.rs
+++ b/core/server/src/args.rs
@@ -38,12 +38,12 @@ DOCUMENTATION:
     https://iggy.apache.org/docs
 
 CONFIGURATION:
-    The server uses a TOML configuration file. By default, it looks for 'configs/server.toml'
+    The server uses a TOML configuration file. By default, it looks for 'core/server/config.toml'
     in the current working directory. You can override this with the IGGY_CONFIG_PATH environment
     variable or use the --config-provider flag.
 
     Examples:
-        iggy-server                                    # Uses default file provider (configs/server.toml)
+        iggy-server                                    # Uses default file provider (core/server/config.toml)
         iggy-server --config-provider file             # Explicitly use file provider
         IGGY_CONFIG_PATH=custom.toml iggy-server       # Use custom config file path
 
diff --git a/core/server/src/configs/cluster.rs b/core/server/src/configs/cluster.rs
index 7e3893650..8468c3a81 100644
--- a/core/server/src/configs/cluster.rs
+++ b/core/server/src/configs/cluster.rs
@@ -16,35 +16,36 @@
  * under the License.
  */
 
+use iggy_common::ConfigEnv;
 use serde::{Deserialize, Serialize};
 
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct ClusterConfig {
     pub enabled: bool,
     pub name: String,
     pub node: NodeConfig,
 }
 
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct NodeConfig {
     pub current: CurrentNodeConfig,
     pub others: Vec<OtherNodeConfig>,
 }
 
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct CurrentNodeConfig {
     pub name: String,
     pub ip: String,
 }
 
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct OtherNodeConfig {
     pub name: String,
     pub ip: String,
     pub ports: TransportPorts,
 }
 
-#[derive(Debug, Deserialize, Serialize, Clone, Default)]
+#[derive(Debug, Deserialize, Serialize, Clone, Default, ConfigEnv)]
 pub struct TransportPorts {
     pub tcp: Option<u16>,
     pub quic: Option<u16>,
diff --git a/core/server/src/configs/defaults.rs b/core/server/src/configs/defaults.rs
index f6d9d83e5..af283442e 100644
--- a/core/server/src/configs/defaults.rs
+++ b/core/server/src/configs/defaults.rs
@@ -42,8 +42,8 @@ use std::sync::Arc;
 use std::time::Duration;
 
 static_toml::static_toml! {
-    // static_toml crate always starts from CARGO_MANIFEST_DIR (in this case iggy-server root directory)
-    pub static SERVER_CONFIG = include_toml!("../configs/server.toml");
+    // static_toml crate always starts from CARGO_MANIFEST_DIR (core/server)
+    pub static SERVER_CONFIG = include_toml!("config.toml");
 }
 
 impl Default for ServerConfig {
diff --git a/core/server/src/configs/http.rs b/core/server/src/configs/http.rs
index 770632170..b21720cfb 100644
--- a/core/server/src/configs/http.rs
+++ b/core/server/src/configs/http.rs
@@ -16,6 +16,7 @@
  * under the License.
  */
 
+use iggy_common::ConfigEnv;
 use iggy_common::IggyByteSize;
 use iggy_common::IggyDuration;
 use iggy_common::IggyError;
@@ -25,10 +26,11 @@ use serde::{Deserialize, Serialize};
 use serde_with::DisplayFromStr;
 use serde_with::serde_as;
 
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct HttpConfig {
     pub enabled: bool,
     pub address: String,
+    #[config_env(leaf)]
     pub max_request_size: IggyByteSize,
     pub web_ui: bool,
     pub cors: HttpCorsConfig,
@@ -37,7 +39,7 @@ pub struct HttpConfig {
     pub tls: HttpTlsConfig,
 }
 
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct HttpCorsConfig {
     pub enabled: bool,
     pub allowed_methods: Vec<String>,
@@ -49,25 +51,30 @@ pub struct HttpCorsConfig {
 }
 
 #[serde_as]
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct HttpJwtConfig {
     pub algorithm: String,
     pub issuer: String,
     pub audience: String,
     pub valid_issuers: Vec<String>,
     pub valid_audiences: Vec<String>,
+    #[config_env(leaf)]
     #[serde_as(as = "DisplayFromStr")]
     pub access_token_expiry: IggyExpiry,
+    #[config_env(leaf)]
     #[serde_as(as = "DisplayFromStr")]
     pub clock_skew: IggyDuration,
+    #[config_env(leaf)]
     #[serde_as(as = "DisplayFromStr")]
     pub not_before: IggyDuration,
+    #[config_env(secret)]
     pub encoding_secret: String,
+    #[config_env(secret)]
     pub decoding_secret: String,
     pub use_base64_secret: bool,
 }
 
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct HttpMetricsConfig {
     pub enabled: bool,
     pub endpoint: String,
@@ -79,7 +86,7 @@ pub enum JwtSecret {
     Base64(String),
 }
 
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct HttpTlsConfig {
     pub enabled: bool,
     pub cert_file: String,
diff --git a/core/server/src/configs/quic.rs b/core/server/src/configs/quic.rs
index f2cedd6de..ee4c28c12 100644
--- a/core/server/src/configs/quic.rs
+++ b/core/server/src/configs/quic.rs
@@ -16,6 +16,7 @@
  * under the License.
  */
 
+use iggy_common::ConfigEnv;
 use iggy_common::IggyByteSize;
 use iggy_common::IggyDuration;
 use serde::{Deserialize, Serialize};
@@ -23,32 +24,40 @@ use serde_with::DisplayFromStr;
 use serde_with::serde_as;
 
 #[serde_as]
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct QuicConfig {
     pub enabled: bool,
     pub address: String,
     pub max_concurrent_bidi_streams: u64,
+    #[config_env(leaf)]
     pub datagram_send_buffer_size: IggyByteSize,
+    #[config_env(leaf)]
     pub initial_mtu: IggyByteSize,
+    #[config_env(leaf)]
     pub send_window: IggyByteSize,
+    #[config_env(leaf)]
     pub receive_window: IggyByteSize,
+    #[config_env(leaf)]
     #[serde_as(as = "DisplayFromStr")]
     pub keep_alive_interval: IggyDuration,
+    #[config_env(leaf)]
     #[serde_as(as = "DisplayFromStr")]
     pub max_idle_timeout: IggyDuration,
     pub certificate: QuicCertificateConfig,
     pub socket: QuicSocketConfig,
 }
 
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct QuicSocketConfig {
     pub override_defaults: bool,
+    #[config_env(leaf)]
     pub recv_buffer_size: IggyByteSize,
+    #[config_env(leaf)]
     pub send_buffer_size: IggyByteSize,
     pub keepalive: bool,
 }
 
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct QuicCertificateConfig {
     pub self_signed: bool,
     pub cert_file: String,
diff --git a/core/server/src/configs/server.rs b/core/server/src/configs/server.rs
index 70008c4f7..f3fe593ce 100644
--- a/core/server/src/configs/server.rs
+++ b/core/server/src/configs/server.rs
@@ -16,7 +16,6 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-use crate::IGGY_ROOT_PASSWORD_ENV;
 use crate::configs::COMPONENT;
 use crate::configs::cluster::ClusterConfig;
 use crate::configs::http::HttpConfig;
@@ -25,13 +24,13 @@ use crate::configs::system::SystemConfig;
 use crate::configs::tcp::TcpConfig;
 use crate::configs::websocket::WebSocketConfig;
 use crate::server_error::ConfigurationError;
+use configs::{ConfigEnvMappings, ConfigProvider, FileConfigProvider, TypedEnvProvider};
 use derive_more::Display;
 use err_trail::ErrContext;
 use figment::providers::{Format, Toml};
 use figment::value::Dict;
 use figment::{Metadata, Profile, Provider};
-use iggy_common::{ConfigProvider, IggyByteSize, MemoryPoolConfigOther, Validatable};
-use iggy_common::{CustomEnvProvider, FileConfigProvider, IggyDuration};
+use iggy_common::{ConfigEnv, IggyByteSize, IggyDuration, MemoryPoolConfigOther, Validatable};
 use serde::{Deserialize, Serialize};
 use serde_with::DisplayFromStr;
 use serde_with::serde_as;
@@ -39,17 +38,10 @@ use std::env;
 use std::str::FromStr;
 use std::sync::Arc;
 
-const DEFAULT_CONFIG_PATH: &str = "configs/server.toml";
-const SECRET_KEYS: [&str; 6] = [
-    IGGY_ROOT_PASSWORD_ENV,
-    "IGGY_DATA_MAINTENANCE_ARCHIVER_S3_KEY_SECRET",
-    "IGGY_HTTP_JWT_ENCODING_SECRET",
-    "IGGY_HTTP_JWT_DECODING_SECRET",
-    "IGGY_TCP_TLS_PASSWORD",
-    "IGGY_SYSTEM_ENCRYPTION_KEY",
-];
-
-#[derive(Debug, Deserialize, Serialize, Clone)]
+const DEFAULT_CONFIG_PATH: &str = "core/server/config.toml";
+
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
+#[config_env(prefix = "IGGY_", name = "iggy-server-config")]
 pub struct ServerConfig {
     pub data_maintenance: DataMaintenanceConfig,
     pub message_saver: MessageSaverConfig,
@@ -65,13 +57,11 @@ pub struct ServerConfig {
 }
 
 /// Configuration for the memory pool.
-#[derive(Debug, Deserialize, Serialize)]
+#[derive(Debug, Deserialize, Serialize, ConfigEnv)]
 pub struct MemoryPoolConfig {
-    /// Whether the pool is enabled.
     pub enabled: bool,
-    /// Maximum size of the pool.
+    #[config_env(leaf)]
     pub size: IggyByteSize,
-    /// Maximum number of buffers per bucket.
     pub bucket_capacity: u32,
 }
 
@@ -87,51 +77,55 @@ impl MemoryPoolConfig {
 }
 
 #[serde_as]
-#[derive(Debug, Default, Deserialize, Serialize, Clone)]
+#[derive(Debug, Default, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct DataMaintenanceConfig {
     pub messages: MessagesMaintenanceConfig,
 }
 
 #[serde_as]
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct MessagesMaintenanceConfig {
     pub cleaner_enabled: bool,
+    #[config_env(leaf)]
     #[serde_as(as = "DisplayFromStr")]
     pub interval: IggyDuration,
 }
 
 #[serde_as]
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct MessageSaverConfig {
     pub enabled: bool,
     pub enforce_fsync: bool,
+    #[config_env(leaf)]
     #[serde_as(as = "DisplayFromStr")]
     pub interval: IggyDuration,
 }
 
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct PersonalAccessTokenConfig {
     pub max_tokens_per_user: u32,
     pub cleaner: PersonalAccessTokenCleanerConfig,
 }
 
 #[serde_as]
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct PersonalAccessTokenCleanerConfig {
     pub enabled: bool,
+    #[config_env(leaf)]
     #[serde_as(as = "DisplayFromStr")]
     pub interval: IggyDuration,
 }
 
 #[serde_as]
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct HeartbeatConfig {
     pub enabled: bool,
+    #[config_env(leaf)]
     #[serde_as(as = "DisplayFromStr")]
     pub interval: IggyDuration,
 }
 
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct TelemetryConfig {
     pub enabled: bool,
     pub service_name: String,
@@ -139,14 +133,16 @@ pub struct TelemetryConfig {
     pub traces: TelemetryTracesConfig,
 }
 
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct TelemetryLogsConfig {
+    #[config_env(leaf)]
     pub transport: TelemetryTransport,
     pub endpoint: String,
 }
 
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct TelemetryTracesConfig {
+    #[config_env(leaf)]
     pub transport: TelemetryTransport,
     pub endpoint: String,
 }
@@ -172,6 +168,9 @@ impl FromStr for TelemetryTransport {
 }
 
 impl ServerConfig {
+    /// Load server configuration from file and environment variables.
+    ///
+    /// Uses compile-time generated env var mappings for unambiguous resolution.
     pub async fn load() -> Result<ServerConfig, ConfigurationError> {
         let config_path =
             env::var("IGGY_CONFIG_PATH").unwrap_or_else(|_| DEFAULT_CONFIG_PATH.to_string());
@@ -180,19 +179,20 @@ impl ServerConfig {
             config_provider
                 .load_config()
                 .await
-                .error(|e: &iggy_common::ConfigurationError| {
-                    format!("{COMPONENT} (error: {e}) - failed to load config provider config")
+                .error(|e: &configs::ConfigurationError| {
+                    format!("{COMPONENT} (error: {e}) - failed to load config")
                 })?;
         server_config
             .validate()
-            .error(|e: &iggy_common::ConfigurationError| {
+            .error(|e: &configs::ConfigurationError| {
                 format!("{COMPONENT} (error: {e}) - failed to validate server config")
             })?;
         Ok(server_config)
     }
 
+    /// Create a config provider using compile-time generated env var mappings.
     pub fn config_provider(config_path: &str) -> FileConfigProvider<ServerConfigEnvProvider> {
-        let default_config = Toml::string(include_str!("../../../configs/server.toml"));
+        let default_config = Toml::string(include_str!("../../config.toml"));
         FileConfigProvider::new(
             config_path.to_string(),
             ServerConfigEnvProvider::default(),
@@ -200,29 +200,40 @@ impl ServerConfig {
             Some(default_config),
         )
     }
+
+    /// Returns all valid environment variable names for ServerConfig.
+    pub fn all_env_var_names() -> Vec<&'static str> {
+        <ServerConfig as ConfigEnvMappings>::all_env_var_names()
+    }
 }
 
+/// Type-safe environment provider using compile-time generated mappings.
+///
+/// Uses the `ConfigEnvMappings` trait generated by `#[derive(ConfigEnv)]`
+/// to directly look up known environment variable names, eliminating path ambiguity.
 #[derive(Debug, Clone)]
 pub struct ServerConfigEnvProvider {
-    provider: CustomEnvProvider<ServerConfig>,
+    provider: TypedEnvProvider<ServerConfig>,
 }
 
 impl Default for ServerConfigEnvProvider {
     fn default() -> Self {
         Self {
-            provider: CustomEnvProvider::new("IGGY_", &SECRET_KEYS),
+            provider: TypedEnvProvider::from_config(ServerConfig::ENV_PREFIX),
         }
     }
 }
 
 impl Provider for ServerConfigEnvProvider {
     fn metadata(&self) -> Metadata {
-        Metadata::named("iggy-server config")
+        Metadata::named(ServerConfig::ENV_PROVIDER_NAME)
     }
 
     fn data(&self) -> Result<figment::value::Map<Profile, Dict>, figment::Error> {
-        self.provider.deserialize().map_err(|_| {
-            figment::Error::from("Cannot deserialize environment variables for server config")
+        self.provider.deserialize().map_err(|e| {
+            figment::Error::from(format!(
+                "Cannot deserialize environment variables for server config: {e}"
+            ))
         })
     }
 }
diff --git a/core/server/src/configs/sharding.rs b/core/server/src/configs/sharding.rs
index 7891b8161..5b9d28718 100644
--- a/core/server/src/configs/sharding.rs
+++ b/core/server/src/configs/sharding.rs
@@ -31,10 +31,12 @@ use std::thread::available_parallelism;
 use tracing::info;
 
 use crate::server_error::ServerError;
+use iggy_common::ConfigEnv;
 
-#[derive(Debug, Deserialize, Serialize, Default)]
+#[derive(Debug, Deserialize, Serialize, Default, ConfigEnv)]
 pub struct ShardingConfig {
     #[serde(default)]
+    #[config_env(leaf)]
     pub cpu_allocation: CpuAllocation,
 }
 
diff --git a/core/server/src/configs/system.rs b/core/server/src/configs/system.rs
index cc45ba59d..35ba9ed55 100644
--- a/core/server/src/configs/system.rs
+++ b/core/server/src/configs/system.rs
@@ -19,6 +19,7 @@
 use super::cache_indexes::CacheIndexesConfig;
 use super::sharding::ShardingConfig;
 use crate::configs::server::MemoryPoolConfig;
+use iggy_common::ConfigEnv;
 use iggy_common::IggyByteSize;
 use iggy_common::IggyError;
 use iggy_common::IggyExpiry;
@@ -31,7 +32,7 @@ use serde_with::serde_as;
 pub const INDEX_EXTENSION: &str = "index";
 pub const LOG_EXTENSION: &str = "log";
 
-#[derive(Debug, Deserialize, Serialize)]
+#[derive(Debug, Deserialize, Serialize, ConfigEnv)]
 pub struct SystemConfig {
     pub path: String,
     pub backup: BackupConfig,
@@ -50,104 +51,116 @@ pub struct SystemConfig {
     pub sharding: ShardingConfig,
 }
 
-#[derive(Debug, Deserialize, Serialize)]
+#[derive(Debug, Deserialize, Serialize, ConfigEnv)]
 pub struct BackupConfig {
     pub path: String,
     pub compatibility: CompatibilityConfig,
 }
 
-#[derive(Debug, Deserialize, Serialize)]
+#[derive(Debug, Deserialize, Serialize, ConfigEnv)]
 pub struct CompatibilityConfig {
     pub path: String,
 }
 
-#[derive(Debug, Deserialize, Serialize)]
+#[derive(Debug, Deserialize, Serialize, ConfigEnv)]
 pub struct DatabaseConfig {
     pub path: String,
 }
 
-#[derive(Debug, Deserialize, Serialize)]
+#[derive(Debug, Deserialize, Serialize, ConfigEnv)]
 pub struct RuntimeConfig {
     pub path: String,
 }
 
-#[derive(Debug, Deserialize, Serialize)]
+#[derive(Debug, Deserialize, Serialize, ConfigEnv)]
 pub struct CompressionConfig {
     pub allow_override: bool,
+    #[config_env(leaf)]
     pub default_algorithm: CompressionAlgorithm,
 }
 
 #[serde_as]
-#[derive(Debug, Deserialize, Serialize)]
+#[derive(Debug, Deserialize, Serialize, ConfigEnv)]
 pub struct LoggingConfig {
     pub path: String,
     pub level: String,
     pub file_enabled: bool,
+    #[config_env(leaf)]
     pub max_size: IggyByteSize,
+    #[config_env(leaf)]
     #[serde_as(as = "DisplayFromStr")]
     pub retention: IggyDuration,
+    #[config_env(leaf)]
     #[serde_as(as = "DisplayFromStr")]
     pub sysinfo_print_interval: IggyDuration,
 }
 
-#[derive(Debug, Deserialize, Serialize)]
+#[derive(Debug, Deserialize, Serialize, ConfigEnv)]
 pub struct EncryptionConfig {
     pub enabled: bool,
+    #[config_env(secret)]
     pub key: String,
 }
 
-#[derive(Debug, Deserialize, Serialize)]
+#[derive(Debug, Deserialize, Serialize, ConfigEnv)]
 pub struct StreamConfig {
     pub path: String,
 }
 
 #[serde_as]
-#[derive(Debug, Deserialize, Serialize)]
+#[derive(Debug, Deserialize, Serialize, ConfigEnv)]
 pub struct TopicConfig {
     pub path: String,
+    #[config_env(leaf)]
     #[serde_as(as = "DisplayFromStr")]
     pub max_size: MaxTopicSize,
     pub delete_oldest_segments: bool,
 }
 
-#[derive(Debug, Deserialize, Serialize)]
+#[derive(Debug, Deserialize, Serialize, ConfigEnv)]
 pub struct PartitionConfig {
     pub path: String,
     pub messages_required_to_save: u32,
+    #[config_env(leaf)]
     pub size_of_messages_required_to_save: IggyByteSize,
     pub enforce_fsync: bool,
     pub validate_checksum: bool,
 }
 
 #[serde_as]
-#[derive(Debug, Deserialize, Serialize)]
+#[derive(Debug, Deserialize, Serialize, ConfigEnv)]
 pub struct MessageDeduplicationConfig {
     pub enabled: bool,
     pub max_entries: u64,
+    #[config_env(leaf)]
     #[serde_as(as = "DisplayFromStr")]
     pub expiry: IggyDuration,
 }
 
-#[derive(Debug, Deserialize, Serialize)]
+#[derive(Debug, Deserialize, Serialize, ConfigEnv)]
 pub struct RecoveryConfig {
     pub recreate_missing_state: bool,
 }
 
 #[serde_as]
-#[derive(Debug, Deserialize, Serialize)]
+#[derive(Debug, Deserialize, Serialize, ConfigEnv)]
 pub struct SegmentConfig {
+    #[config_env(leaf)]
     pub size: IggyByteSize,
+    #[config_env(leaf)]
     pub cache_indexes: CacheIndexesConfig,
+    #[config_env(leaf)]
     #[serde_as(as = "DisplayFromStr")]
     pub message_expiry: IggyExpiry,
     pub archive_expired: bool,
 }
 
 #[serde_as]
-#[derive(Debug, Deserialize, Serialize)]
+#[derive(Debug, Deserialize, Serialize, ConfigEnv)]
 pub struct StateConfig {
     pub enforce_fsync: bool,
     pub max_file_operation_retries: u32,
+    #[config_env(leaf)]
     #[serde_as(as = "DisplayFromStr")]
     pub retry_delay: IggyDuration,
 }
diff --git a/core/server/src/configs/tcp.rs b/core/server/src/configs/tcp.rs
index 02ec8bbd5..fcc7d2de6 100644
--- a/core/server/src/configs/tcp.rs
+++ b/core/server/src/configs/tcp.rs
@@ -16,12 +16,12 @@
  * under the License.
  */
 
-use iggy_common::{IggyByteSize, IggyDuration};
+use iggy_common::{ConfigEnv, IggyByteSize, IggyDuration};
 use serde::{Deserialize, Serialize};
 use serde_with::DisplayFromStr;
 use serde_with::serde_as;
 
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct TcpConfig {
     pub enabled: bool,
     pub address: String,
@@ -31,7 +31,7 @@ pub struct TcpConfig {
     pub socket_migration: bool,
 }
 
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct TcpTlsConfig {
     pub enabled: bool,
     pub self_signed: bool,
@@ -40,13 +40,16 @@ pub struct TcpTlsConfig {
 }
 
 #[serde_as]
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct TcpSocketConfig {
     pub override_defaults: bool,
+    #[config_env(leaf)]
     pub recv_buffer_size: IggyByteSize,
+    #[config_env(leaf)]
     pub send_buffer_size: IggyByteSize,
     pub keepalive: bool,
     pub nodelay: bool,
+    #[config_env(leaf)]
     #[serde_as(as = "DisplayFromStr")]
     pub linger: IggyDuration,
 }
diff --git a/core/server/src/configs/validators.rs b/core/server/src/configs/validators.rs
index af12a4102..987b0d86a 100644
--- a/core/server/src/configs/validators.rs
+++ b/core/server/src/configs/validators.rs
@@ -28,11 +28,12 @@ use crate::configs::server::{MemoryPoolConfig, PersonalAccessTokenConfig, Server
 use crate::configs::sharding::NumaTopology;
 use crate::configs::system::SegmentConfig;
 use crate::streaming::segments::*;
+use configs::ConfigurationError;
 use err_trail::ErrContext;
+use iggy_common::CompressionAlgorithm;
 use iggy_common::IggyExpiry;
 use iggy_common::MaxTopicSize;
 use iggy_common::Validatable;
-use iggy_common::{CompressionAlgorithm, ConfigurationError};
 use std::thread::available_parallelism;
 use tracing::{error, warn};
 
@@ -41,17 +42,17 @@ impl Validatable<ConfigurationError> for ServerConfig {
         self.system
             .memory_pool
             .validate()
-            .error(|e: &iggy_common::ConfigurationError| {
+            .error(|e: &ConfigurationError| {
                 format!("{COMPONENT} (error: {e}) - failed to validate memory pool config")
             })?;
         self.data_maintenance
             .validate()
-            .error(|e: &iggy_common::ConfigurationError| {
+            .error(|e: &ConfigurationError| {
                 format!("{COMPONENT} (error: {e}) - failed to validate data maintenance config")
             })?;
         self.personal_access_token
             .validate()
-            .error(|e: &iggy_common::ConfigurationError| {
+            .error(|e: &ConfigurationError| {
                 format!(
                     "{COMPONENT} (error: {e}) - failed to validate personal access token config"
                 )
@@ -59,31 +60,27 @@ impl Validatable<ConfigurationError> for ServerConfig {
         self.system
             .segment
             .validate()
-            .error(|e: &iggy_common::ConfigurationError| {
+            .error(|e: &ConfigurationError| {
                 format!("{COMPONENT} (error: {e}) - failed to validate segment config")
             })?;
         self.system
             .compression
             .validate()
-            .error(|e: &iggy_common::ConfigurationError| {
+            .error(|e: &ConfigurationError| {
                 format!("{COMPONENT} (error: {e}) - failed to validate compression config")
             })?;
-        self.telemetry
-            .validate()
-            .error(|e: &iggy_common::ConfigurationError| {
-                format!("{COMPONENT} (error: {e}) - failed to validate telemetry config")
-            })?;
+        self.telemetry.validate().error(|e: &ConfigurationError| {
+            format!("{COMPONENT} (error: {e}) - failed to validate telemetry config")
+        })?;
         self.system
             .sharding
             .validate()
-            .error(|e: &iggy_common::ConfigurationError| {
+            .error(|e: &ConfigurationError| {
                 format!("{COMPONENT} (error: {e}) - failed to validate sharding config")
             })?;
-        self.cluster
-            .validate()
-            .error(|e: &iggy_common::ConfigurationError| {
-                format!("{COMPONENT} (error: {e}) - failed to validate cluster config")
-            })?;
+        self.cluster.validate().error(|e: &ConfigurationError| {
+            format!("{COMPONENT} (error: {e}) - failed to validate cluster config")
+        })?;
 
         let topic_size = match self.system.topic.max_size {
             MaxTopicSize::Custom(size) => Ok(size.as_bytes_u64()),
@@ -222,11 +219,9 @@ impl Validatable<ConfigurationError> for MessageSaverConfig {
 
 impl Validatable<ConfigurationError> for DataMaintenanceConfig {
     fn validate(&self) -> Result<(), ConfigurationError> {
-        self.messages
-            .validate()
-            .error(|e: &iggy_common::ConfigurationError| {
-                format!("{COMPONENT} (error: {e}) - failed to validate messages maintenance config")
-            })?;
+        self.messages.validate().error(|e: &ConfigurationError| {
+            format!("{COMPONENT} (error: {e}) - failed to validate messages maintenance config")
+        })?;
         Ok(())
     }
 }
diff --git a/core/server/src/configs/websocket.rs b/core/server/src/configs/websocket.rs
index 2c9c996ce..b911f4c15 100644
--- a/core/server/src/configs/websocket.rs
+++ b/core/server/src/configs/websocket.rs
@@ -16,12 +16,13 @@
  * under the License.
  */
 
+use iggy_common::ConfigEnv;
 use iggy_common::IggyByteSize;
 use serde::{Deserialize, Serialize};
 use std::fmt::{Display, Formatter};
 use tungstenite::protocol::WebSocketConfig as TungsteniteConfig;
 
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct WebSocketConfig {
     pub enabled: bool,
     pub address: String,
@@ -41,7 +42,7 @@ pub struct WebSocketConfig {
     pub tls: WebSocketTlsConfig,
 }
 
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct WebSocketTlsConfig {
     pub enabled: bool,
     pub self_signed: bool,
diff --git a/core/server/src/server_error.rs b/core/server/src/server_error.rs
index f49e10468..2be3135ab 100644
--- a/core/server/src/server_error.rs
+++ b/core/server/src/server_error.rs
@@ -68,7 +68,7 @@ error_set!(
     }
 
     ConfigurationError := {
-        ConfigurationError(iggy_common::ConfigurationError),
+        ConfigurationError(configs::ConfigurationError),
     }
 
     ArchiverError := {
diff --git a/examples/java/README.md b/examples/java/README.md
index 1589d5c15..41d2644b4 100644
--- a/examples/java/README.md
+++ b/examples/java/README.md
@@ -33,7 +33,7 @@ Iggy requires valid credentials to authenticate client requests. The examples as
 > **Note** <br>
 > This setup is intended only for development and testing, not production use.
 
-By default, all server data is stored in the `local_data` directory (this can be changed via `system.path` in `server.toml`).
+By default, all server data is stored in the `local_data` directory (this can be changed via `system.path` in `config.toml`).
 
 Root credentials are applied **only on the very first startup**, when no data directory exists yet.
 Once the server has created and populated the data directory, the existing stored credentials will always be used, and supplying the `--with-default-root-credentials` flag or setting the environment variables will no longer override them.
diff --git a/examples/rust/README.md b/examples/rust/README.md
index c406c30a4..b1de83d06 100644
--- a/examples/rust/README.md
+++ b/examples/rust/README.md
@@ -31,7 +31,7 @@ Iggy requires valid credentials to authenticate client requests. The examples as
 > **Note** <br>
 > This setup is intended only for development and testing, not production use.
 
-By default, all server data is stored in the `local_data` directory (this can be changed via `system.path` in `server.toml`).
+By default, all server data is stored in the `local_data` directory (this can be changed via `system.path` in `config.toml`).
 
 Root credentials are applied **only on the very first startup**, when no data directory exists yet.
 Once the server has created and populated the data directory, the existing stored credentials will always be used, and supplying the `--with-default-root-credentials` flag or setting the environment variables will no longer override them.
diff --git a/foreign/node/src/e2e/tcp.cluster.e2e.ts b/foreign/node/src/e2e/tcp.cluster.e2e.ts
index 61f3d47e5..e3317f80d 100644
--- a/foreign/node/src/e2e/tcp.cluster.e2e.ts
+++ b/foreign/node/src/e2e/tcp.cluster.e2e.ts
@@ -22,7 +22,7 @@ import assert from "node:assert/strict";
 import { getTestClient } from "./test-client.utils.js";
 
 // cluster mode still in dev atm
-// response is mocked from /core/configs/server.toml
+// response is mocked from /core/server/config.toml
 const expectedMeta = {
   name: "iggy-cluster",
   nodes: [