Files
kandev/docs/decisions/2026-08-01-release-toggle-gating-contract.md
T
2026-08-02 15:36:21 +01:00

79 lines
4.2 KiB
Markdown

# ADR-2026-08-01-release-toggle-gating-contract: Release Toggles Are Install-Wide Fail-Closed Gates
**Status:** proposed
**Date:** 2026-08-01
**Area:** backend, frontend, workflow
## Context
Kandev already resolves startup feature values from explicit environment,
persisted installation overrides, and `profiles.yaml`. Adding a frontend-visible
flag still duplicates its identity and config behavior across backend maps,
switches, frontend types, and defaults. More importantly, a UI preference or a
single late behavior check does not make a risky PR safe when direct callers,
agent tools, background work, shared initialization, or migrations can bypass
the gate.
## Decision
Temporary release toggles use the existing install-wide runtime-flag system;
they are not represented as portable per-user preferences. New release toggles
ship off in `prod`, `dev`, and `e2e`, and dedicated tests or selected
installations opt in explicitly.
The backend is authoritative. Every externally reachable entry path is gated at
its narrowest composition or capability boundary, and disabled requests fail
before mutation or dispatch. Frontend checks control discovery and presentation
but never substitute for backend enforcement. Schema changes, startup work, and
shared refactors remain independently safe because code executed outside the
gate is not protected by it.
The internal registration in `internal/runtimeflags/registry.go` becomes the
single backend binding for registry key, environment variable, config read, and
config apply behavior; the public `RuntimeFlagDefinition` remains metadata-only.
Generic resolution iterates registrations instead of adding each flag to
separate maps and switches. The frontend derives its feature-name type and
all-false defaults from one declaration. CI requires exact registry,
`FeaturesConfig`, profile, and frontend-default key equality and validates
registration metadata and typed binding isolation.
Rollout is staged: merge default-off, enable selected installations, ship one
default-on release with the kill switch retained, then remove the flag and
legacy behavior. Graduation moves the key and environment variable to the
append-only retired-identity set in `internal/runtimeflags/registry.go`;
retired identities are never reused. Unknown persisted overrides are ignored
rather than deleted so downgrade compatibility does not mutate operator state.
## Consequences
- Medium and large changes can merge without changing default behavior, while a
real installation can opt in through the existing admin UI or environment.
- Direct clients and asynchronous entry paths cannot bypass an off flag.
- Flag addition and removal have fewer mechanical edit sites and generic tests
catch incomplete plumbing.
- Each gated feature still needs deliberate rollback-safe data and migration
design; a flag is not a substitute for compatibility.
- Retaining the kill switch for one default-on release delays complete code
removal, but preserves rollback during the highest-exposure stage.
- Unknown override rows can accumulate, but remain inert and preserve downgrade
behavior. The source-controlled retired-identity set prevents stale database
or environment values from acquiring new meaning.
## Alternatives Considered
1. **Use per-user preferences as release controls.** Rejected because rollout
ownership is installation-wide, direct callers can bypass UI preferences,
and portable settings add persistence plumbing that must later be removed.
2. **Keep the current per-flag maps and switches.** Rejected because each flag
multiplies addition/removal sites and makes drift likely as gated PR volume
grows.
3. **Generate flag code from YAML.** Rejected for now because code generation
adds build and review machinery when a definition-owned binding removes the
duplication without generated artifacts.
4. **Use reflection in production to bind `FeaturesConfig`.** Rejected because
explicit typed bindings are easier to navigate and fail at compile time;
reflection remains acceptable in completeness tests.
5. **Adopt an external rollout service.** Rejected because Kandev needs
installation opt-in and a local kill switch, not cohorts, percentage rollout,
or a hosted control plane.