Runtime Feature Flags
Use this checklist for temporary release toggles and risky features. It is
self-contained: do not depend on the agent having read an ADR or public docs.
Read apps/backend/AGENTS.md and apps/web/AGENTS.md when the change touches
those subtrees.
Invariants
- The backend is authoritative. A disabled feature must not be reachable through
HTTP, WebSocket, MCP, agent-tool, or background-job entry points.
- A new release toggle is off in every shipped profile when it is merged. The
disabled path preserves the existing behavior and fails closed before deriving
data, writing state, dispatching work, or exposing a capability.
- Effective values are ordered: explicit environment variable, SQLite override,
then profile default. An explicit environment variable locks the admin UI.
- Use one identity across layers:
features.<camelCaseKey>,
KANDEV_FEATURES_<UPPER_SNAKE_CASE>, a Go FeaturesConfig field, its JSON tag,
and the matching frontend key. Never add a parallel flag map or switch.
- Never reuse an identity recorded in the repository's append-only retired
registry. Before relying on retirement, verify that
retiredRuntimeFlagIdentities
and its collision/completeness test exist; if they do not, add that
runtimeflags infrastructure before removing a live flag.
Add a flag
Update these layers in the same change:
- Profile default: add
KANDEV_FEATURES_<NAME> under features: in the
root profiles.yaml; use prod: "false", dev: "false", and e2e: "false"
for a release toggle unless the task explicitly documents a test-only
exception.
- Backend config: add a
bool field with explicit mapstructure and
json tags to apps/backend/internal/common/config/config.go.
- Registry/config binding: add exactly one metadata registration to
apps/backend/internal/runtimeflags/registry.go: key, env var, kind, label,
description, stability, risk, and restart/mutability metadata. The registry
definition is metadata-only; add the key/env constants and update
OptionsFromConfig, ValuesFromConfig, and ApplyStatesToConfig in
runtimeflags/config.go for typed config wiring.
- Backend gates: gate construction and every enabled-only entry path at the
narrow composition boundary. Do not only hide the frontend; direct callers
must receive the legacy behavior or a safe rejection.
- Frontend contract: add the all-off key to
apps/web/lib/state/slices/features/types.ts. Use useFeature() for client
surfaces and notFound() from the relevant server layout/page when a route
subtree must be unavailable. SSR data must remain fail-closed.
- Tests: add enabled/disabled behavior tests for each changed backend path
and frontend surface. Run the existing registry/profile/frontend contract
tests; add focused tests for normalization, route visibility, and disabled
side-effect prevention where applicable.
The completeness checks require exact equality between profile keys, typed
backend fields/registrations, and frontend default keys. They do not discover
semantic call sites, so trace the feature's HTTP, WebSocket, MCP, agent, worker,
and startup paths manually.
Roll out and promote
- Merge with all shipped profile defaults off.
- Enable one installation through Settings > System > Feature Toggles or an
explicit environment variable, restart when metadata requires it, and test
real workflows.
- When ready for everyone, change only the
prod profile value to "true" for
the next release. Retain the registry entry and backend/frontend gates as a
kill switch so operators can still disable the feature.
Graduate and remove the flag
After the feature has proven itself as the default-on behavior, make the new
behavior unconditional and remove the live flag end-to-end:
- remove the profile entry and
FeaturesConfig field;
- remove the active registry registration;
- remove backend conditionals and legacy branches;
- remove the frontend default,
useFeature() checks, and route gates;
- remove flag-specific tests and documentation while keeping permanent behavior
coverage.
Before removing the registration, verify the append-only
retiredRuntimeFlagIdentities registry and its collision/completeness test
exist, then append the exact key and environment variable in registry.go:
{key: "features.example", envVar: "KANDEV_FEATURES_EXAMPLE"},
Do not delete old runtime_flag_overrides rows as part of graduation. Unknown
rows are intentionally inert, and the retired identity prevents stale operator
state from reactivating a future feature. Never reuse either the key or env var.
Verification and handoff
Run the focused checks appropriate to the change:
- In a fresh worktree, run
pnpm install --frozen-lockfile from apps/ before
any pnpm-based checks, tests, lint, or commits.
- from
apps/backend: go test ./internal/runtimeflags ./internal/common/config ./internal/profiles;
- from the repository root:
make -C apps/backend lint;
- from
apps: pnpm --filter @kandev/web test -- lib/state/slices/features/features-contract.test.ts;
- from
apps/web: pnpm run typecheck and pnpm run lint;
- run affected E2E coverage when the gated surface is user-visible;
- run
git diff --check before handoff.
Report the flag key/env identity, profile defaults, every gated entry path,
disabled/enabled test evidence, restart requirements, and whether the change is
still a kill switch or has been fully graduated.
1---2name: runtime-feature-flags3description: Add, roll out, promote, graduate, or remove Kandev runtime feature flags and release toggles across the backend and frontend. Use whenever a task mentions a feature flag, release toggle, staged rollout, kill switch, or graduating a flag.4---56# Runtime Feature Flags78Use this checklist for temporary release toggles and risky features. It is9self-contained: do not depend on the agent having read an ADR or public docs.10Read `apps/backend/AGENTS.md` and `apps/web/AGENTS.md` when the change touches11those subtrees.1213## Invariants1415- The backend is authoritative. A disabled feature must not be reachable through16 HTTP, WebSocket, MCP, agent-tool, or background-job entry points.17- A new release toggle is off in every shipped profile when it is merged. The18 disabled path preserves the existing behavior and fails closed before deriving19 data, writing state, dispatching work, or exposing a capability.20- Effective values are ordered: explicit environment variable, SQLite override,21 then profile default. An explicit environment variable locks the admin UI.22- Use one identity across layers: `features.<camelCaseKey>`,23 `KANDEV_FEATURES_<UPPER_SNAKE_CASE>`, a Go `FeaturesConfig` field, its JSON tag,24 and the matching frontend key. Never add a parallel flag map or switch.25- Never reuse an identity recorded in the repository's append-only retired26 registry. Before relying on retirement, verify that `retiredRuntimeFlagIdentities`27 and its collision/completeness test exist; if they do not, add that28 runtimeflags infrastructure before removing a live flag.2930## Add a flag3132Update these layers in the same change:33341. **Profile default:** add `KANDEV_FEATURES_<NAME>` under `features:` in the35 root `profiles.yaml`; use `prod: "false"`, `dev: "false"`, and `e2e: "false"`36 for a release toggle unless the task explicitly documents a test-only37 exception.382. **Backend config:** add a `bool` field with explicit `mapstructure` and39 `json` tags to `apps/backend/internal/common/config/config.go`.403. **Registry/config binding:** add exactly one metadata registration to41 `apps/backend/internal/runtimeflags/registry.go`: key, env var, kind, label,42 description, stability, risk, and restart/mutability metadata. The registry43 definition is metadata-only; add the key/env constants and update44 `OptionsFromConfig`, `ValuesFromConfig`, and `ApplyStatesToConfig` in45 `runtimeflags/config.go` for typed config wiring.464. **Backend gates:** gate construction and every enabled-only entry path at the47 narrow composition boundary. Do not only hide the frontend; direct callers48 must receive the legacy behavior or a safe rejection.495. **Frontend contract:** add the all-off key to50 `apps/web/lib/state/slices/features/types.ts`. Use `useFeature()` for client51 surfaces and `notFound()` from the relevant server layout/page when a route52 subtree must be unavailable. SSR data must remain fail-closed.536. **Tests:** add enabled/disabled behavior tests for each changed backend path54 and frontend surface. Run the existing registry/profile/frontend contract55 tests; add focused tests for normalization, route visibility, and disabled56 side-effect prevention where applicable.5758The completeness checks require exact equality between profile keys, typed59backend fields/registrations, and frontend default keys. They do not discover60semantic call sites, so trace the feature's HTTP, WebSocket, MCP, agent, worker,61and startup paths manually.6263## Roll out and promote64651. Merge with all shipped profile defaults off.662. Enable one installation through **Settings > System > Feature Toggles** or an67 explicit environment variable, restart when metadata requires it, and test68 real workflows.693. When ready for everyone, change only the `prod` profile value to `"true"` for70 the next release. Retain the registry entry and backend/frontend gates as a71 kill switch so operators can still disable the feature.7273## Graduate and remove the flag7475After the feature has proven itself as the default-on behavior, make the new76behavior unconditional and remove the live flag end-to-end:7778- remove the profile entry and `FeaturesConfig` field;79- remove the active registry registration;80- remove backend conditionals and legacy branches;81- remove the frontend default, `useFeature()` checks, and route gates;82- remove flag-specific tests and documentation while keeping permanent behavior83 coverage.8485Before removing the registration, verify the append-only86`retiredRuntimeFlagIdentities` registry and its collision/completeness test87exist, then append the exact key and environment variable in `registry.go`:8889```go90{key: "features.example", envVar: "KANDEV_FEATURES_EXAMPLE"},91```9293Do not delete old `runtime_flag_overrides` rows as part of graduation. Unknown94rows are intentionally inert, and the retired identity prevents stale operator95state from reactivating a future feature. Never reuse either the key or env var.9697## Verification and handoff9899Run the focused checks appropriate to the change:100101- In a fresh worktree, run `pnpm install --frozen-lockfile` from `apps/` before102 any pnpm-based checks, tests, lint, or commits.103- from `apps/backend`: `go test ./internal/runtimeflags ./internal/common/config ./internal/profiles`;104- from the repository root: `make -C apps/backend lint`;105- from `apps`: `pnpm --filter @kandev/web test -- lib/state/slices/features/features-contract.test.ts`;106- from `apps/web`: `pnpm run typecheck` and `pnpm run lint`;107- run affected E2E coverage when the gated surface is user-visible;108- run `git diff --check` before handoff.109110Report the flag key/env identity, profile defaults, every gated entry path,111disabled/enabled test evidence, restart requirements, and whether the change is112still a kill switch or has been fully graduated.