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>
77 lines
3.9 KiB
Markdown
77 lines
3.9 KiB
Markdown
# 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](../project/backlog/WP-47-feature-flags.md); see
|
|
[ADR-0004](architecture/0004-stamdata-as-code.md) 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](../project/backlog/WP-47-feature-flags.md) — the build.
|
|
- [ADR-0004](architecture/0004-stamdata-as-code.md) — 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](roles-and-access.md) — `flags:manage` + admin gating.
|