Skip to content

Add config store support across all adapters#209

Merged
prk-Jr merged 18 commits intomainfrom
feat/config-store
Apr 8, 2026
Merged

Add config store support across all adapters#209
prk-Jr merged 18 commits intomainfrom
feat/config-store

Conversation

@prk-Jr
Copy link
Copy Markdown
Contributor

@prk-Jr prk-Jr commented Mar 9, 2026

Summary

  • Introduces a portable ConfigStore abstraction in edgezero-core that lets handlers read key/value configuration at runtime without coupling to a specific platform
  • Implements platform-native backing stores for Fastly (edge dictionary), Cloudflare (KV / env bindings), and Axum (in-memory map with env-var fallback), all sharing a common contract verified by shared test macros
  • Wires config store injection through the #[app] macro and each adapter's request pipeline so handlers receive a ConfigStoreHandle via RequestContext with no boilerplate

Changes

Crate / File Change
edgezero-core/src/config_store.rs New ConfigStore trait, ConfigStoreHandle wrapper, and shared contract test macro
edgezero-core/src/manifest.rs New manifest module with ConfigStore TOML binding and adapter name resolution
edgezero-core/src/context.rs Added config_store() accessor and injection helpers to RequestContext
edgezero-core/src/app.rs App::build hooks extended to accept config store configuration
edgezero-core/src/lib.rs Re-exported manifest module
edgezero-adapter-axum/src/config_store.rs New: in-memory + env-var AxumConfigStore with defaults support
edgezero-adapter-axum/src/service.rs Inject ConfigStoreHandle into each request before routing
edgezero-adapter-axum/src/dev_server.rs Accept optional config store handle in DevServerConfig
edgezero-adapter-axum/src/lib.rs Re-exported config store types
edgezero-adapter-fastly/src/config_store.rs New: FastlyConfigStore backed by Fastly edge dictionary
edgezero-adapter-fastly/src/lib.rs Re-exported and wired config store into dispatch path
edgezero-adapter-fastly/src/request.rs Inject config store handle during request conversion
edgezero-adapter-fastly/tests/contract.rs Updated contract tests
edgezero-adapter-cloudflare/src/config_store.rs New: CloudflareConfigStore backed by worker::Env bindings
edgezero-adapter-cloudflare/src/lib.rs Re-exported and wired config store into dispatch path
edgezero-adapter-cloudflare/src/request.rs Inject config store handle during request conversion
edgezero-adapter-cloudflare/tests/contract.rs Updated contract tests
edgezero-macros/src/app.rs #[app] macro generates config store setup from manifest
examples/app-demo/ Added config store usage in demo handlers and adapter wiring
docs/guide/ Added config store adapter docs and configuration guide
scripts/smoke_test_config.sh New smoke test script for config store end-to-end verification
.gitignore Excluded generated WASM artifacts

Closes

Closes #51

Test plan

  • cargo test --workspace --all-targets
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo check --workspace --all-targets --features "fastly cloudflare"
  • WASM builds: wasm32-wasip1 (Fastly) / wasm32-unknown-unknown (Cloudflare)
  • Manual testing via edgezero-cli dev
  • Other: shared contract test macro verified against all three adapter implementations

Checklist

  • Changes follow CLAUDE.md conventions
  • No Tokio deps added to core or adapter crates
  • Route params use {id} syntax (not :id)
  • Types imported from edgezero_core (not http crate)
  • New code has tests
  • No secrets or credentials committed

@prk-Jr prk-Jr self-assigned this Mar 9, 2026
Copy link
Copy Markdown
Contributor

@ChristianPavilonis ChristianPavilonis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the cross-adapter config store work — the overall direction looks good. I’m requesting changes for one high-severity issue plus follow-ups.

Findings

  1. High — Axum config store can expose full process environment (secret leakage risk)

    • crates/edgezero-adapter-axum/src/config_store.rs:12
    • crates/edgezero-adapter-axum/src/config_store.rs:37
    • examples/app-demo/crates/app-demo-core/src/handlers.rs:119
    • AxumConfigStore::from_env snapshots all env vars, so any handler pattern that accepts user-controlled keys can accidentally expose unrelated secrets.
    • Requested fix: replace blanket std::env::vars() capture with an explicit allowlist (manifest-declared keys only), and avoid arbitrary key-lookup patterns in examples intended for production-like usage.
  2. Medium — Adapter override key casing is inconsistent across resolution paths

    • crates/edgezero-core/src/manifest.rs:352
    • crates/edgezero-macros/src/app.rs:120
    • crates/edgezero-core/src/app.rs:59
    • Mixed-case adapter keys can work in one path and fail in another.
    • Requested fix: normalize keys at parse/finalize time (or enforce lowercase with validation) and add a mixed-case adapter-key test.
  3. Low — Missing positive-path injection coverage in adapter tests

    • crates/edgezero-adapter-fastly/tests/contract.rs:17
    • crates/edgezero-adapter-cloudflare/tests/contract.rs:188
    • Please add success-path assertions that config store injection/retrieval works when bindings are present.

Once the high-severity item is addressed, this should be in good shape.

Copy link
Copy Markdown
Contributor

@aram356 aram356 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Config Store Feature

Overall this is a well-structured feature that follows the existing adapter pattern cleanly. The core trait, contract test macro, per-adapter implementations, and manifest/macro integration are all thoughtfully designed. Test coverage is solid across all three adapters and the docs are thorough.

That said, I found issues across four areas that should be addressed before merge — one high-severity security concern, two medium design issues, and one CI coverage gap.

Summary

Severity Finding
High Axum config-store exposes entire process environment (secret leakage risk)
Medium Case handling for adapter overrides is inconsistent between manifest and metadata paths
Medium dispatch() bypasses config-store injection, diverging from run_app behavior
Medium-Low New WASM adapter code paths are weakly exercised in CI

See inline comments for details and suggested improvements.

Copy link
Copy Markdown
Contributor

@ChristianPavilonis ChristianPavilonis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up review complete. No new issues were found in the current changeset, and previously noted concerns appear addressed.

Copy link
Copy Markdown
Contributor

@aram356 aram356 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review

Summary

Solid config store abstraction with good test coverage across all three adapters. The contract test macro and error mapping are well-designed. Main concerns are around the public dispatch API silently dropping KV handles, and a dual config resolution path that needs clarification.

Findings

Blocking

  • 🔧 dispatch_with_config / dispatch_with_config_handle silently drop KV: Both Fastly and Cloudflare versions pass None for KV. Users migrating from dispatch or dispatch_with_kv to a config-aware path will lose KV access with no warning. (cloudflare request.rs:106-130, fastly request.rs:78-103)
  • Dual config name resolution in run_app: Both adapters check A::config_store() (compile-time) then fall back to runtime manifest parsing. Since both derive from the same edgezero.toml, when would these diverge? Needs documentation or removal of dead path. (cloudflare lib.rs:86-94, fastly lib.rs:97-107)

Non-blocking

  • 🤔 CloudflareConfigStore::new() silent fallback: A binding typo gives an empty store with only a log::warn. Consider renaming to new_or_empty() or removing in favor of try_new only.
  • ♻️ Duplicate bounded dedup caches: Fastly RecentStringSet and Cloudflare ConfigCache are structurally identical. Candidate for a shared core utility.
  • _ctx prefix on used variable: cloudflare/tests/contract.rs:55 — underscore implies unused but variable is read.
  • 🏕 wasm-bindgen-test in [dependencies]: Pre-existing but file was touched — test-only crate shouldn't be in production deps.
  • 🌱 Consider a ConfigStore extractor: Handlers call ctx.config_store() manually; a ConfigStore(store) extractor (like Kv) would complete the pattern.

📌 Out of Scope

  • wasm-bindgen-test dep placement should be fixed in a separate cleanup PR
  • Bounded cache dedup is a candidate for future core utility extraction

CI Status

  • fmt: PASS
  • clippy: PASS
  • tests: PASS

dispatch_with_config and dispatch_with_config_handle in both the
Cloudflare and Fastly adapters were passing None for the KV handle,
silently dropping KV access for callers on those paths. Both now
resolve the default KV binding/store (non-required) alongside the
config store.

Additional cleanup from review:
- Document why run_app has two config-name resolution paths
  (macro-generated vs. manual Hooks impls)
- Rename CloudflareConfigStore::new() to new_or_empty() to make
  the silent fallback-to-empty behavior explicit
- Fix _ctx prefix on an actively-read variable in cloudflare
  contract tests
- Move wasm-bindgen-test to [dev-dependencies]
@prk-Jr prk-Jr requested a review from aram356 March 25, 2026 05:01
Copy link
Copy Markdown
Contributor

@aram356 aram356 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review

Summary

Well-structured, well-tested portable ConfigStore abstraction with implementations for Fastly, Cloudflare, and Axum. The contract test macro, security boundaries (env-var allowlisting, demo handler key allowlist), and deprecation strategy are all strong. CI passes cleanly. Two blocking issues around a silent behavioral change to ServiceUnavailable formatting and a logic bug in warning deduplication.

😃 Praise

  • Contract test macro (config_store_contract_tests!) with configurable #[$test_attr] is excellent — enables wasm_bindgen_test for Cloudflare while others use #[test]
  • AxumConfigStore::from_env only reads declared keys — strong security boundary preventing the config store from becoming an env-var oracle
  • Demo handler allowlist (ALLOWED_CONFIG_KEYS) — great documentation-by-example of the recommended security practice
  • Deprecation strategydispatch() deprecated with clear migration path via dispatch_raw and new dispatch variants
  • CI additions — explicit cloudflare-wasm-tests and fastly-wasm-tests jobs with proper wasm-bindgen-cli version resolution

Findings

Blocking

  • 🔧 ServiceUnavailable format string change: #[error("service unavailable: {message}")]#[error("{message}")] removes the prefix for ALL callers, not just config store — silent behavioral change (crates/edgezero-core/src/error.rs:22)
  • 🔧 RecentStringSet::insert logic bug: returns true without tracking the key when limit == 0, defeating dedup (crates/edgezero-adapter-fastly/src/request.rs:227)

Non-blocking

  • 🤔 contract_empty_key_returns_none may not match real Fastly behavior — real try_get("") returns KeyInvalid error, not Ok(None) (crates/edgezero-core/src/config_store.rs:168)
  • 🤔 Dispatch function proliferation — 7 dispatch-related functions per adapter; consider an options struct pattern as store types grow
  • 🤔 Cloudflare ConfigCache caches None for invalid JSON permanently — correct but should document why caching failure is safe (isolate immutability)
  • ♻️ Duplicated bounded LRU patternRecentStringSet (Fastly) and ConfigCache (Cloudflare) implement the same HashMap+VecDeque eviction; extract if it appears a third time
  • warned_store_cache() wrapper — function wrapping a static is unnecessary indirection; could inline in warn_missing_store_once

📌 Out of Scope

  • Spin adapter missing — PR title says "across all adapters" but Spin is excluded from SUPPORTED_CONFIG_STORE_ADAPTERS. Consider adjusting title or creating a follow-up issue
  • 🌱 Spin config store supportspin_sdk::variables::get() maps well to this abstraction; worth a follow-up issue
  • 📝 Cloudflare JSON-in-TOML approach — creative workaround for platform binding-name limitations; DX could be smoothed by CLI tooling generating JSON from TOML defaults

CI Status

  • fmt: ✅ PASS
  • clippy: ✅ PASS
  • tests: ✅ PASS

- Revert #[error("{message}")] back to #[error("service unavailable: {message}")]
  to restore the Display/to_string() prefix for all ServiceUnavailable callers
- Fix RecentStringSet::insert: limit==0 path returned true without tracking
  the key, defeating dedup; always insert into keys/order, evict only when
  limit > 0
- Document why caching None is safe in CloudflareConfigStore lookup_cached
  (Cloudflare bindings are immutable within an isolate lifetime)
- Inline warned_store_cache() static into warn_missing_store_once, matching
  the pattern used by warn_missing_kv_store_once
@prk-Jr prk-Jr requested a review from aram356 March 31, 2026 07:48
aram356

This comment was marked as low quality.

aram356

This comment was marked as low quality.

aram356

This comment was marked as low quality.

aram356

This comment was marked as low quality.

aram356

This comment was marked as low quality.

aram356

This comment was marked as low quality.

aram356

This comment was marked as low quality.

Copy link
Copy Markdown
Contributor

@aram356 aram356 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review

Summary

This PR introduces a well-designed portable ConfigStore abstraction with implementations across Axum, Fastly, and Cloudflare adapters. The overall architecture is clean, follows established project patterns, and has solid test coverage. However, it needs a rebase onto current main to pick up the Spin adapter (#166), and there are a few gaps in CI placement and test coverage.

Praise

  • 😃 Synchronous ConfigStore trait is the right call for all current backends — avoids unnecessary async in WASM
  • 😃 Contract test macro (config_store_contract_tests!) with configurable test attribute is a great reusable pattern
  • 😃 Axum from_lookup dependency injection is textbook testability design
  • 😃 Consistent resolve → inject → dispatch flow across all three adapters makes the codebase easy to navigate
  • 😃 Cloudflare module-level docs excellently explain the JSON-in-string-binding approach with concrete examples
  • 😃 Two-path config resolution comments in Fastly adapter prevent future confusion
  • 😃 Feature gating (serde_json behind cloudflare feature) is correct
  • 😃 Demo app handler with allowlist check, graceful degradation, and proper error propagation
  • 😃 Smoke test script is well-structured with cleanup trap, readiness polling, and pass/fail summary

Findings

Blocking

  • 🔧 Spin adapter missing throughout: SUPPORTED_CONFIG_STORE_ADAPTERS in manifest.rs:57 omits "spin", and SPIN_ADAPTER constant is missing from app.rs. Branch needs rebase onto main (#166). CI gate 4 (cargo check --features "fastly cloudflare spin") fails. (see inline comments)
  • 🔧 Cloudflare wasm check misplaced in CI: .github/workflows/test.yml:170 — the step cargo check -p edgezero-adapter-cloudflare --features cloudflare --target wasm32-unknown-unknown is in the fastly-wasm-tests job instead of cloudflare-wasm-tests. If the Fastly job fails early, this check never runs. This also forces the Fastly job to install wasm32-unknown-unknown unnecessarily. Fix: Move this step to the cloudflare-wasm-tests job.
  • 🔧 Missing test for store-level key miss: handlers.rs:458config_get_returns_404_when_key_missing tests the allowlist, not store.get() returning None. The store-miss code path (lines 162-165) has zero test coverage. (see inline comment)

Non-blocking

  • Axum skips A::config_store() hooks path (dev_server.rs:219): Fastly and Cloudflare use two-path resolution (compile-time hooks → manifest fallback), Axum only reads the manifest. Cross-adapter behavioral inconsistency. (see inline comment)
  • 🤔 contract_empty_key_returns_none may not hold on all backends (config_store.rs:168): Fastly Config Store SDK may error on empty keys rather than returning None. (see inline comment)
  • 🤔 No convenience re-exports for config_store types (lib.rs:6): key_value_store gets pub use re-exports but config_store doesn't. (see inline comment)
  • 🤔 new_or_empty silently swallows missing bindings (cloudflare/config_store.rs:37): Returns empty store with no warning when binding is missing. (see inline comment)
  • ♻️ Unbounded BTreeSet in Fastly KV warn-once (fastly/request.rs:248): Config store warn-once uses bounded RecentStringSet (64 entries) but the pre-existing KV variant uses an unbounded BTreeSet. Unify on the bounded pattern.
  • 🏕 Missing test for ConfigStoreError::Internal conversion (error.rs:120): Unavailable and InvalidKey covered, Internal not. (see inline comment)
  • Test name misleading (handlers.rs:458): config_get_returns_404_when_key_missing tests allowlist, not store miss. Consider renaming to config_get_returns_404_when_key_not_in_allowlist.

Out of Scope

  • 📌 RecentStringSet duplicated across Fastly and Cloudflare adapters — future refactor to share in core
  • 📌 serve_with_listener grew to 5 positional params — consider struct param if a third store type is added
  • 📌 KV adapter keys lack the same validation as config store keys (pre-existing)
  • 📌 Spin adapter config store implementation (separate PR after rebase)

CI Status

  • fmt: PASS
  • clippy: PASS
  • tests: PASS (478 tests)
  • feature check (spin): FAIL — branch needs rebase onto main

@prk-Jr prk-Jr requested a review from aram356 April 3, 2026 12:46
Copy link
Copy Markdown
Contributor

@aram356 aram356 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review

Summary

Solid feature addition that introduces a portable ConfigStore abstraction following the established adapter pattern (mirrors KV store). Code is clean, well-tested (335 tests pass), and CI is green. A few items need resolution before merge.

😃 Praise

  • Excellent contract test macro design with platform-specific #[$test_attr] — ensures cross-adapter consistency while supporting WASM test frameworks
  • Security-conscious AxumConfigStore::from_env that only reads env vars for declared keys, with an explicit test proving DATABASE_URL doesn't leak
  • CI upgrade splitting WASM contract tests into dedicated jobs that execute on real targets, not just compile-check
  • WASM safety documentation on std::sync::Mutex usage in Cloudflare isolates
  • Demo handler ALLOWED_CONFIG_KEYS allowlist sets a good security example for users
  • Clean layered dispatch API in Cloudflare with clear deprecation paths

Findings

Blocking

  • Axum Hooks::config_store() asymmetry: Fastly/Cloudflare check A::config_store() first; Axum ignores it entirely. Needs confirmation this is intentional + user-facing docs (dev_server.rs:313)

Non-blocking

  • 🤔 "spin" in SUPPORTED_CONFIG_STORE_ADAPTERS: No Spin implementation exists — silent misconfiguration trap (manifest.rs:57)
  • 🤔 --force on wasm-bindgen-cli install: Wastes ~2-3 min per CI run recompiling from source (test.yml:117)
  • 🤔 ConfigCache::insert is really get_or_insert: Method name is misleading; double-Option return is subtle (cloudflare/config_store.rs:143)
  • ♻️ Duplicate Fastly warning helpers: warn_missing_store_once and warn_missing_kv_store_once are ~30 lines of near-identical code with inconsistent parameter types (fastly/request.rs:196-251)
  • ⛏ Doc comment for ConfigStore trait placed on ConfigStoreError enum (core/config_store.rs:16)
  • ⛏ Redundant Other match arm above wildcard (fastly/config_store.rs:56)
  • ⛏ Duplicate node_modules/ in .gitignore (line 2 and 12)
  • APP_CONFIG vs app_config case inconsistency in docs (configuration.md:154)

🌱 Future Considerations

  • No size limit on Cloudflare cached JSON payloads — platform limits mitigate this but a max-size guard could be added later
  • Manual sync burden between wrangler.toml / fastly.toml and edgezero.toml defaults — a future edgezero sync-config CLI command could help

📌 Out of Scope

  • Spin adapter missing from "Available Adapters" table in docs/guide/adapters/overview.md (pre-existing gap from PR #166)

CI Status

  • fmt: PASS
  • clippy: PASS
  • tests: PASS (335 tests)

prk-Jr added 2 commits April 6, 2026 12:20
Introduce Stores {config_store, kv, secrets} in the Axum dev server,
Fastly adapter, and Cloudflare adapter, replacing dispatch_with_handles
signatures that took 3–7 positional Option<T> arguments. Add
StoreRequirements {kv_required, secrets_required} to Fastly lib to
eliminate positional bool swap risk in run_app_with_stores.

AxumDevServer gains with_kv_handle and with_secret_handle builders,
making all three stores configurable without going through run_app.
The run_with_listener test helper is simplified to use the builders
instead of accepting a raw kv_path string.

The duplicate store_handles.rs files (identical logic, different cfg
guards) are removed; insertion is now inlined into dispatch_core_request
via the Stores struct. Also fix kv_store_name to reference
DEFAULT_KV_STORE_NAME directly, add a doc note to Spin run_app about
missing store support, log Fastly SDK lookup errors internally instead
of surfacing them in 503 responses, and document CONFIG_CACHE_LIMIT.
@prk-Jr prk-Jr requested a review from aram356 April 6, 2026 08:09
Copy link
Copy Markdown
Contributor

@aram356 aram356 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review

Summary

Well-structured feature that adds a portable ConfigStore abstraction across Fastly, Cloudflare, and Axum adapters. The contract test macro, Stores struct pattern replacing positional params, and deprecation strategy for dispatch() are all well-executed. After multiple review rounds this PR is in good shape.

😃 Praise

  • Contract test macroconfig_store_contract_tests! with attribute injection (#[wasm_bindgen_test] for Cloudflare, #[test] for others) is an excellent pattern that guarantees behavioral parity across backends.
  • Stores struct replacing positional params — Eliminates a real class of bugs (boolean/option parameter swaps). StoreRequirements in Fastly applies the same good idea to bool args.
  • Demo handler allowlistALLOWED_CONFIG_KEYS plus the doc note "validate or allowlist keys first" is good security awareness and teaching.
  • Compile-time manifest validation — Adding manifest.validate() in the #[app] macro catches misconfigured manifests at build time.
  • Comprehensive test coverage — 557 tests pass. Every adapter has contract tests, core has unit + contract tests, demo handler has 6 test cases covering happy path, missing key, no store, and unavailable store.

Findings

Non-blocking

  • 🤔 Axum silently ignores Hooks::config_store() — Fastly/Cloudflare check A::config_store() first, Axum doesn't. Documented in a comment but creates a behavioral asymmetry. (inline comment)
  • ♻️ RecentStringSet::insert double-allocates keykey.to_string() called twice; could clone from first allocation. (inline comment)
  • 🏕 CF docs direct users to deprecated run_app_with_manifest — New text at line 53 points to a deprecated function. (inline comment)
  • 🏕 CF docs entrypoint missing manifest_src arg — Pre-existing: the example shows run_app::<App>(req, env, ctx) but the real signature takes manifest_src first. Good campsite fix since this PR adds text below it.
  • Stores struct duplicated in 3 modules — Axum dev_server.rs, Cloudflare request.rs, Fastly request.rs define structurally identical structs. Fine today, worth noting if more store types are added.
  • 🤔 Cloudflare lookup_cached acquires mutex twice — Safe on single-threaded WASM, and get_or_insert handles the TOCTOU correctly. Style observation only.
  • 🌱 CI cache key shared across parallel jobs — All three workflow jobs use the same cargo cache key but install different tools. Job-specific suffixes would prevent potential cache conflicts.
  • 🌱 Spin config store — Correctly documented as not yet implemented. Manifest validation rejects [stores.config.adapters.spin].

📌 Out of Scope

  • Config store extractor — A dedicated Config<T> extractor (like ValidatedJson<T>) would improve handler ergonomics but is a separate feature.
  • Config store write support — Current abstraction is read-only by design.

CI Status

  • fmt: PASS
  • clippy: PASS
  • tests: PASS (557 tests)
  • feature check: PASS

@prk-Jr prk-Jr merged commit f6801f3 into main Apr 8, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Config Store Abstraction

3 participants