feat: ADR — unified configuration backend across canopy domains
Description
Canopy has at least 7 configuration domains, each handled differently today. Each shares substantial plumbing concerns (where the file lives, how it's loaded, how it's reloaded, how it's versioned) but each invented its own answer. This issue tracks the cross-canopy ADR that unifies the backend (where config comes from) while leaving the per-domain overlays (precedence, audit, mutation surface) where they are.
Surfaced during ADR-023 ratification for epic &51 (#460) — the worker portal redesign's composition baseline source raised the broader question: is composition special, or is it just the 7th instance of an under-generalized pattern?
Current state — 7 domains, 7 mechanisms
| Domain | Storage | Format | Override | Mutation | Audit | Origin ADR |
|---|---|---|---|---|---|---|
| Service config | config/{svc}/default.yaml + env yaml + env vars |
YAML | env > env-yaml > default (3-tier) | filesystem + env | none | ADR-012 |
| Secrets | secrets/dev.yaml (SOPS) |
YAML encrypted | env > yaml | filesystem encrypted | none | ADR-017 |
| Jurisdiction policy | jurisdiction.toml per-jurisdiction |
TOML | single-tier, PAMMS-cited | filesystem (xtask) | citation audit | ADR-006, ADR-011 |
| Rulesets | rulesets/federal/ + rulesets/{juris}/ |
JDM | federal + per-jurisdiction overlay | filesystem (xtask) | rules engine | ADR-003, ADR-006 |
| Theme | rulesets/{juris}/theme.toml |
TOML | per-jurisdiction only | filesystem | none | (in CLAUDE.md) |
| IDP | rulesets/{juris}/idp.toml (planned #493 (closed)) |
TOML | per-jurisdiction only | filesystem | none | (planned) |
| Composition (ratified 2026-05-20) | rulesets/{juris}/composition/*.toml + DB JSON Patch |
TOML + JSON Patch | 5-layer top-wins | filesystem + DB write API + Studio | JWS AuditEvent | ADR-021/022 |
What's shared vs domain-specific
Shared plumbing — one canopy-wide mechanism could handle all of these:
- Where does the file live? Filesystem, git clone, HTTP URL, S3 bucket
- How do I get the latest version? Boot-time, scheduled refresh, signal-driven, filesystem-watch
- How do I detect schema drift?
deny_unknown_fields, JSON schema validation, etc. - How do I version it? Git SHA, content hash, ETag, mtime
Domain-specific overlays — genuinely different per domain, do not generalize:
- Precedence rules (3-tier env-wins vs 5-tier user-wins vs single-tier vs federal+overlay)
- Mutation surfaces (xtask only vs runtime write API vs encrypted-at-rest)
- Audit requirements (none vs PAMMS citation vs JWS-signed)
- Validation (per-domain schema + semantic checks)
The ADR's question
Define the ConfigBackend (or ConfigSource) trait that every config domain in canopy plugs into. Per-domain overlays (precedence, audit, mutation, validation) stay domain-specific and unchanged.
Concretely, the ADR ratifies:
. The trait shape (read-only contract: load(path) -> Result<Bytes, Error>, version(path) -> Result<Version, Error>, subscribe(path) -> Stream<ChangeEvent>)
. v1 backend impls (likely filesystem only — matches current state)
. v2+ backend impls (git clone with refresh, HTTP, S3)
. Per-domain migration path: how each existing domain (ADR-006/011/012/017) plugs into the new backend without breaking
. Whether write capability is part of the trait (likely separate WritableBackend sub-trait; only some backends are writable)
Acceptance Criteria
-
docs/modules/ROOT/pages/adrs/adr-NNN-unified-config-backend.adocratifying the trait shape + v1 backend(s) + migration path - Cross-references from ADR-006, ADR-011, ADR-012, ADR-017, ADR-021, ADR-022 indicating which config domain plugs in
- No runtime code in the ADR-only MR; implementation MRs follow
- Antora nav updated
Why this came up now
Filed during epic &51 (#460) Stage-2 ADR ratification (composability runtime + storage layering + promote-PR). When drafting ADR-023 (promote-PR mechanism for composition baselines), the question "what is the contract between canopy and the source of composition baselines" became "what is the contract between canopy and the source of any config baseline". Composition isn't special — it's the 7th instance.
ADR-023 was deferred (#488 (closed) closed-deferred); #492 (closed) (Stage-3 promote-live-to-baseline implementation) deferred too. Composition's v1 ships using the existing filesystem rulesets/{juris}/ pattern (Studio writes JSON Patch overrides to DB; "promote" affordance descoped from v1 Studio per #500). This issue defines the broader pattern that future ADRs (revised promote, unified config) compose against.
Context & References
- ADR-006 (jurisdiction-agnostic rulesets) — filesystem only today
- ADR-011 (policy-to-rules pipeline) — PAMMS citation overlay
- ADR-012 (layered YAML configuration) — service-config 3-tier
- ADR-017 (encrypted secrets at rest) — SOPS overlay
- ADR-021 (composability runtime + plugin model) — composition runtime
- ADR-022 (composition override storage layering) — composition overlay
- Epic &51 (#460) — surfaced this question
- Deferred: #488 (closed) (ADR-023 promote-PR), #492 (closed) (Stage-3 promote-live-to-baseline)
Labels
type::feature, priority::medium, program::infrastructure, service::shared-crates, workflow::ready