docs: ADR-0004 stamdata-as-code
CI / frontend (push) Failing after 1m20s
CI / storybook-a11y (push) Failing after 5m27s
CI / e2e (push) Has been cancelled
CI / codeql (csharp) (push) Has been cancelled
CI / codeql (javascript-typescript) (push) Has been cancelled
CI / api-client-drift (push) Has been cancelled
CI / backend (push) Has been cancelled

Document the config-as-code strategy for business-tunable reference data:
typed checked-in config validated at compile time (never a production DB),
where UI text (), reference tables (Stamdata/), and letter content
each live, and why org-templates are the deliberate runtime-editable exception.
Index it in docs/README.md and add a CLAUDE.md pointer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-21 07:41:21 +02:00
co-authored by Claude Opus 4.8
parent fa7e9c5cfb
commit a7f737e18c
3 changed files with 88 additions and 0 deletions
+6
View File
@@ -117,6 +117,12 @@ touches only `infrastructure/` + `contracts/` (see ARCHITECTURE §6). Server-own
rules stay in `domain/*.policy.ts` as reference impl + unit test, marked server-owned, rules stay in `domain/*.policy.ts` as reference impl + unit test, marked server-owned,
but the FE doesn't call them. but the FE doesn't call them.
**Business-tunable reference data ("stamdata") is config-as-code, not a DB.** Tables the
business controls (profession↔diploma map, thresholds, policy-question text) live as typed
C# in `backend/.../Stamdata/`, validated at build by `StamdataValidationTests` (a bad edit
fails CI, never prod) — never runtime-editable. Org-templates are the deliberate exception
(operational per-org config in SQLite). UI copy is `$localize`. See ADR-0004.
### 5. Testing ### 5. Testing
Vitest. Co-locate `*.spec.ts` next to the unit. **Domain and pure logic must have a Vitest. Co-locate `*.spec.ts` next to the unit. **Domain and pure logic must have a
+1
View File
@@ -22,6 +22,7 @@ condensed, cross-linked curriculum.
| [architecture/0001-bff-lite-decision-dtos.md](reference/architecture/0001-bff-lite-decision-dtos.md) | ADR — BFF-lite endpoints + decision DTOs (backend decides, FE renders). | | [architecture/0001-bff-lite-decision-dtos.md](reference/architecture/0001-bff-lite-decision-dtos.md) | ADR — BFF-lite endpoints + decision DTOs (backend decides, FE renders). |
| [architecture/0002-user-groups-and-bounded-contexts.md](reference/architecture/0002-user-groups-and-bounded-contexts.md) | ADR — user groups as actors; identity vs authorization. | | [architecture/0002-user-groups-and-bounded-contexts.md](reference/architecture/0002-user-groups-and-bounded-contexts.md) | ADR — user groups as actors; identity vs authorization. |
| [architecture/0003-cibg-huisstijl.md](reference/architecture/0003-cibg-huisstijl.md) | ADR — adopt CIBG Huisstijl (vendored Bootstrap 5.2) + the token bridge. | | [architecture/0003-cibg-huisstijl.md](reference/architecture/0003-cibg-huisstijl.md) | ADR — adopt CIBG Huisstijl (vendored Bootstrap 5.2) + the token bridge. |
| [architecture/0004-stamdata-as-code.md](reference/architecture/0004-stamdata-as-code.md) | ADR — business-tunable reference data as typed, compile-time-validated config (not a production DB). |
| [fp-tea-atomic-design.md](reference/fp-tea-atomic-design.md) | Long-form learning guide: FP + The Elm Architecture + atomic design. | | [fp-tea-atomic-design.md](reference/fp-tea-atomic-design.md) | Long-form learning guide: FP + The Elm Architecture + atomic design. |
| [wcag-checklist.md](reference/wcag-checklist.md) | Manual WCAG checks automation can't catch (tab order, focus traps, reflow). | | [wcag-checklist.md](reference/wcag-checklist.md) | Manual WCAG checks automation can't catch (tab order, focus traps, reflow). |
| [ui-ux-audit.md](reference/ui-ux-audit.md) | Early UI/UX audit against NL Design System (predates ADR-0003 — read in that light). | | [ui-ux-audit.md](reference/ui-ux-audit.md) | Early UI/UX audit against NL Design System (predates ADR-0003 — read in that light). |
@@ -0,0 +1,81 @@
# ADR-0004 — Stamdata as code (config-as-code, not a production database)
Status: Accepted · Date: 2026-07-20
## Context
The business needs to control certain inputs that change over time — the clearest example
being **which professions link to which diplomas** (`geneeskunde → Arts`, …), but also
tunable thresholds, policy-question text, document-category definitions, and some letter
copy. Two hard constraints:
1. **No managing this through a production database.** A live admin surface writing DB rows
means a bad value ships silently and is discovered in production.
2. **Issues must be caught at compile time.** A change should be typed, reviewed, and
versioned before it can affect anyone.
The codebase already leans this way but had never named it as a pattern, and one key table
was neither isolated nor validated:
- All reference data and thresholds are **compiled-in C# constants**, served through
screen-shaped BFF-lite endpoints; the frontend renders decisions and holds no reference
data (ADR-0001).
- User-facing UI copy is already **`$localize`** (`src/locale/*.xlf`) — git-tracked, and a
second locale is a translation file, not a code change. That is already the compile-time
model for text.
- The profession↔diploma map lived as a *private* `Dictionary` inside `DiplomaRules`, mixed
in with the rules that consume it, with **no cross-reference check**: a diploma whose
program wasn't in the map silently rendered `"Onbekend"`.
## Decision
Treat business-tunable reference data as **stamdata-as-code**: typed, checked-in
configuration, changed through the normal git → PR → build → deploy pipeline. Never a
production database, never runtime-editable.
1. **One home, typed.** Business-editable reference data lives in the
`BigRegister.Stamdata` namespace (`backend/src/BigRegister.Api/Stamdata/`), one file per
concern, as plain typed C# data (records / dictionaries). Separate the **data** (what the
business tunes) from the **rules** (dev-owned logic that consumes it): the profession
*table* is `Stamdata.Professions`; the *rule* "an English diploma needs a B2 question"
stays in `DiplomaRules`.
2. **Served unchanged.** The existing BFF-lite endpoints keep serving this data
(`/duo/diplomas`, `/intake/policy`, `/uploads/categories`, …). No frontend change — the
FE still renders decisions.
3. **Two gates.** The **C# compiler** catches shape and type mistakes. A build-time
**`StamdataValidationTests`** catches the referential integrity the compiler can't —
every seeded diploma program resolves to a real profession, no blank keys/values,
thresholds in range. CI runs it, so a bad edit fails the build and never merges.
4. **Business control = config-as-code (GitOps).** The business owns the content of these
files; a change is a reviewed edit, not a live DB write. A future low-code editor could
commit a PR on their behalf without changing this model (the compile-time gate stays).
### Where each kind of business-controllable thing lives
| Kind | Home | Gate |
| --- | --- | --- |
| Reference tables + tunable numbers (professions↔diplomas, thresholds, policy questions, document categories) | `Stamdata/` typed C# | compiler + `StamdataValidationTests` |
| User-facing UI copy | `$localize``src/locale/*.xlf` | build (`i18nMissingTranslation: error`) |
| Letter / brief passage content | config-as-code in the backend (seed content), **not** the DB | compiler + endpoint tests |
### The deliberate exception: org-templates
Per-organization letterhead (return address, footer, signature, margins) **is**
runtime-editable in SQLite, via the org-template admin editor (WP-23/26). That is
intentional and does not contradict this ADR: it is *operational configuration* owned by an
admin persona, versioned with publish/rollback inside the app, and specific to one
sub-organization's identity — not the shared business rules a wrong value would break for
everyone. Stamdata (the rules and reference tables the whole register runs on) stays code.
## Consequences
- **+** Every change is typed, reviewed, versioned, and rollback-able through git; zero
production-DB risk; a dangling reference fails the build with a clear message instead of
reaching users.
- **** A change needs the PR pipeline — not instant, and a non-developer may need dev
assistance to edit C# (mitigated later by a low-code editor that emits a PR, or by a
data-file format if hand-editing ergonomics ever outweigh maximal compile-time safety).
- **Pilot shipped with this ADR:** the profession↔diploma map extracted to
`Stamdata/Professions.cs`, `DiplomaRules` refactored to consume it (behaviour unchanged),
and `StamdataValidationTests` added. Policy-question text and document-category
definitions follow the same pattern as obvious next steps; not moved yet.