Files
ehoandClaude Opus 5 44dcc69811 refactor: ui/dashboard becomes ui/overzicht-secties (RD-36)
RD-03 moved the dashboard page to overzicht/ui/overzicht.page.ts and left four
sections in registratie/ui/dashboard/. The folder was named after a page that
lives in another context. A reader who opened it found four sections that are
not the dashboard.

The folder is now overzicht-secties/ — registratie's sections for the overzicht
page. The alias does not change, because the sections stay in the registratie
context. The story titles do not change, because they name the context.

Five documents cited registratie/ui/dashboard.page.ts, a file that RD-03
renamed. They now name overzicht.page.ts, or the section that owns the
behaviour they describe.

The /dashboard route keeps its path. It is a user-visible URL.

npm run ci --full passes: 67 and 45 storybook suites, 306 axe tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:21:15 +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.
  • overzicht/ui/wat-wilt-u-doen.section.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.