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>
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;IsEnabledis 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 byFlagsAdmin; 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_OPENkey constant) →shared/infrastructure/feature-flags.adapter.ts(list()/set()+parseFlags) →shared/application/feature-flags.store.ts(root singleton mirroringAccessStore; loads once;enabled(key)is deny-by-default and reactive).beheer/ui/feature-flags.page.ts— the admin toggle at/beheer/functies, gated onflags:manage.
How to add a flag
- Add a
FeatureFlagDeftoFeatureFlags.csand a key constant toshared/domain/feature-flag.ts. - Enforce it server-side at the endpoint the flag protects (return 403 when off) — this is the non-negotiable half.
- Gate the UI surface(s) by reading
store.enabled(KEY). - 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 & access —
flags:manage+ admin gating.