Files
atomic-design-poc/docs/reference/feature-flags.md
ehoandClaude Opus 4.8 cf69d474cd
CI / frontend (push) Failing after 1m31s
CI / backend (push) Successful in 1m48s
CI / e2e (push) Successful in 4m19s
CI / storybook-a11y (push) Failing after 7m21s
CI / semgrep (push) Successful in 1m1s
CI / api-client-drift (push) Successful in 3m1s
docs: reference guides for stamdata, audit log, feature flags + document-feature skill
Three how-it-works/how-to-extend reference docs (docs/reference/), each with a
coupling section, indexed in docs/README.md. New document-feature skill so docs
ship in the same diff as the code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 13:39:32 +02:00

3.9 KiB

Feature flags — how they're built, extended & (de)coupled

Runtime on/off switches for features. The split, deliberately mirroring stamdata: the catalog is config-as-code (build-validated), the on/off state is runtime (SQLite, like org-templates). Enforcement is end-to-end — the FE hides the surface and the backend refuses the action. Built in WP-47; see ADR-0004 for the catalog-vs-state reasoning.

The one rule: hiding is not enforcing

A flag gates the UI for feel and the endpoint for real. Flipping inschrijving-open off both removes the "Inschrijven" nav/dashboard action and makes POST /applications return 403. Never gate only the UI — a hidden button is not a closed door.

Layered pipeline

Backend:

  • Domain/Features/FeatureFlags.cs — the code catalog (FeatureFlagDef(Key, Description, DefaultEnabled)). One flag today: inschrijving-open (default on).
  • Data/FeatureFlagStore.cs — stores only overrides in SQLite; All() overlays catalog defaults with overrides; IsEnabled is fail-closed on unknown keys; Set(key, …) rejects unknown keys (→ 404).
  • Program.cs: GET /flags (readable by any principal — drives FE gating), PUT /admin/flags/{key} gated by FlagsAdmin; the server-side enforcement lives at the guarded endpoint (POST /applications → 403 when off).

Frontend — note this feature lives in shared, not beheer (it's consumed app-wide):

  • shared/domain/feature-flag.ts (FeatureFlag + FLAG_INSCHRIJVING_OPEN key constant) → shared/infrastructure/feature-flags.adapter.ts (list()/set() + parseFlags) → shared/application/feature-flags.store.ts (root singleton mirroring AccessStore; loads once; enabled(key) is deny-by-default and reactive).
  • beheer/ui/feature-flags.page.ts — the admin toggle at /beheer/functies, gated on flags:manage.

How to add a flag

  1. Add a FeatureFlagDef to FeatureFlags.cs and a key constant to shared/domain/feature-flag.ts.
  2. Enforce it server-side at the endpoint the flag protects (return 403 when off) — this is the non-negotiable half.
  3. Gate the UI surface(s) by reading store.enabled(KEY).
  4. Cover it in backend/tests/BigRegister.Tests/FeatureFlagTests.cs (default, admin-only toggle, unknown-key 404, off→403 / on→201).

Coupling — the one to watch

This is the most-coupled of the three admin features, and the honest teaching point. Each UI consumer injects FeatureFlagStore, imports the flag-key constant, and hand-writes its own gating predicate inline:

  • shared/layout/site-header/site-header.component.ts — filters the "Inschrijven" nav item.
  • registratie/ui/dashboard.page.ts — hides the "Inschrijven" dashboard action.

So a second flag with a second consumer repeats the pattern by hand — there's no shared "gate this thing by flag" abstraction. That's fine at one flag / two consumers (a helper for a single case is speculative). Recommended: when a second flag lands, extract a small gateByFlag(items, key) / a structural directive rather than growing more inline .filter(… || enabled(KEY)) copies. Document the intent now; don't build the abstraction until the second case forces it.

Wire-up gotcha

PUT /admin/flags must be in the role.interceptor ROLE_AWARE list or the toggle 403s silently; the public GET /flags needs no X-Role. Admin nav lives in ADMIN_LINKS (flags:manage).

See also

  • WP-47 — the build.
  • ADR-0004 — catalog-as-code vs runtime state.
  • backend/src/BigRegister.Api/Domain/Features/FeatureFlags.cs — the catalog.
  • src/app/shared/application/feature-flags.store.ts — the deny-by-default store.
  • Roles & accessflags:manage + admin gating.