`npm run format:check` (a CI gate) had drifted red across 44 files — pre-existing files plus recently-added ones committed without formatting. Ran `prettier --write .`; no logic changes. Also regenerates documentation.json (compodoc reflects the reformatted component sources). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7.1 KiB
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:
- 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.
- 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
DictionaryinsideDiplomaRules, 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.
- One home, typed. Business-editable reference data lives in the
BigRegister.Stamdatanamespace (backend/src/BigRegister.Api/Stamdata/), one file per concern. A table lives either as plain typed C# data (records / dictionaries) or as a typed JSON data-file deserialized into a record (professions.json→ProfessionMapping, loaded viaStamdataFile). Both are checked-in config-as-code, gated the same way; the data-file trades the compiler's value check (gate #1 sees only the shape, not a wrongberoep) for hand-editing ergonomics and the low-code editor below — the value gate becomesStamdataValidationTests. Separate the data (what the business tunes) from the rules (dev-owned logic that consumes it): the profession table isStamdata.Professions; the rule "an English diploma needs a B2 question" stays inDiplomaRules. Tables may carry valid-time (geldigVan/geldigTot, half-open[van, tot));StamdataCatalog+StamdataTable.Of<T>describe every table generically (columns reflected from the record) so one endpoint pair and one grid editor serve all of them. - 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. - Two gates. The C# compiler catches shape and type mistakes. A build-time
StamdataValidationTestscatches 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. - 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# or typed JSON data-file (professions.json), optionally valid-timed |
compiler (shape; + values when C#) + StamdataValidationTests (values, references, validity windows) |
| 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).
- Shipped with this ADR: the profession↔diploma map (
Stamdata/Professions.cs) and the policy-question wording (Stamdata/PolicyQuestions.cs) extracted fromDiplomaRules, which now consumes both (behaviour unchanged), guarded byStamdataValidationTests. Document-category definitions follow the same pattern as the obvious next step; not moved yet. - Shipped as a follow-on (WP-29): the "future low-code editor" and "data-file format" this
ADR floated are now real.
professionsmoved toprofessions.json(typed, valid-timed) and the genericStamdataCatalog/StamdataTable/StamdataFilemodel plus read-only, admin-gatedGET /stamdataendpoints back an Angularbeheer/stamdataeditor. It is not a runtime write path: the admin edits a grid and downloads the edited JSON to commit as a reviewed PR — the compile/validation gate stays the authority, so this ADR's core decision is unchanged.