Records how to model Zorgverlener (SSP), Behandelaar (backoffice), and future actors: personas are actors, not contexts; two capability contexts (Zelfbediening + Behandeling) as separate apps over one backend-owned aanvraag aggregate, integrating via ADR-0001 decision DTOs; identity (typed Principal union in auth) separated from authorization (backend-authoritative). Boundaries only — no code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7.1 KiB
ADR 0002 — User groups as actors, not bounded contexts
Status: Proposed · Date: 2026-07-01
Problem
Today the app knows exactly one actor. auth/domain/session.ts is a flat
Session { bsn, naam }, authentication is a faked DigiD flow, and the backend has no
role model at all (only an X-Admin: true header seam in Program.cs and a stringly-typed
Actor on audit entries). This whole repo is the Zorgverlener self-service portal (SSP).
We now need a second user group — Behandelaar (backoffice: assessing and deciding on applications) — and want room for others later (admin, auditor, institution rep). The question is a modelling one, not a coding one:
How do user groups map onto our DDD structure? Is "Zorgverlener" a bounded context? Is "Behandelaar" a folder next to
registratie/herregistratie? Where does "who may do what" live?
Getting this wrong is expensive: split the code by role and every feature smears across
"folders per persona"; lump everyone into one users context and it becomes a god-context.
Confirmed constraints (with the product owner):
- The backoffice is a separate frontend application, own audience, own deployment.
- The groups authenticate differently: Zorgverlener via DigiD/BSN; Behandelaar via employee SSO.
- Both act on the same underlying aggregate — the aanvraag/registration — but see different views.
Options considered
| Option | Ubiquitous language respected? | Coupling | Verdict |
|---|---|---|---|
1. Split contexts by role (zorgverlener/, behandelaar/ folders) |
No — role ≠ capability; features smear across personas | High | Reject |
2. One catch-all users/identity context owning everything about people |
No — becomes a god-context; mixes identity, authz, and features | High | Reject |
| 3. Actors are personas; contexts are capabilities; identity is typed | Yes | Low | Adopt |
Decision
A user group is an actor, not a bounded context. Bounded contexts are drawn by ubiquitous language + capability, never by who logs in. Concretely:
1. Two capability contexts, two apps, one shared backend domain
The same real-world thing is described in two different languages:
- Zelfbediening (SSP) — the Zorgverlener: "ik vraag herregistratie aan" — eligibility, fill in my data, upload documents, submit. This repo.
- Behandeling (backoffice) — the Behandelaar: "ik beoordeel de aanvraag" — werkvoorraad, beoordeling, besluit, meer-info-opvragen, SLA, audit. A sibling application, not a folder here.
Diverging verbs over the same noun is the textbook signal for two bounded contexts.
2. The aggregate is owned by the backend; the contexts integrate through it
The aanvraag/registration is the system of record in the backend domain. Neither frontend owns it. They integrate through the backend using the BFF-lite decision DTOs of ADR-0001 — the same aggregate projected into two screen-shaped views. The aanvraag status lifecycle is the published contract between the two contexts:
Ingediend → In behandeling → (Meer info gevraagd ⇄) → Goedgekeurd / Afgewezen
The Behandeling context advances this lifecycle; the SSP reads it. Today the SSP already holds
the seed of it — pendingHerregistratie in big-profile.store.ts:53 is the first, coarsest read of
that status ("in behandeling"). As the backoffice appears, that single boolean grows into a real
status the backend publishes.
graph TD
subgraph FE["Frontend bounded contexts (separate apps)"]
SSP["<b>Zelfbediening (SSP)</b><br/>Zorgverlener · DigiD/BSN<br/><i>this repo</i>"]
BO["<b>Behandeling (backoffice)</b><br/>Behandelaar · employee SSO<br/><i>sibling app</i>"]
end
BE["<b>Backend domain</b><br/>aanvraag aggregate (system of record)<br/>status lifecycle · authorization"]
SSP -- "reads aanvraag status<br/>(decision DTOs, ADR-0001)" --> BE
BO -- "advances aanvraag status<br/>(decision DTOs, ADR-0001)" --> BE
classDef c fill:#e5f1fb,stroke:#007bc7,color:#00567d;
classDef d fill:#fff4e5,stroke:#e8830c,color:#8a4b00;
class SSP,BO c;
class BE d;
Both FE contexts are Customer/Conformist to the backend's published aanvraag model. This is deliberately not a Shared Kernel between the two apps — coupling two audiences' codebases directly would defeat the point of splitting them.
3. Separate identity from authorization
These are two concerns people habitually conflate; keeping them apart is the crux of the model.
-
Identity — "who are you, how did you log in" → the
authcontext. Model the principal as a discriminated union, the same "make illegal states unrepresentable" reflex asRemoteData:type Principal = | { kind: 'zorgverlener'; bsn: string; naam: string } // DigiD/BSN | { kind: 'medewerker'; medewerkerId: string; naam: string; rollen: Rol[] }; // employee SSOThe union captures that the two actors authenticate differently and carry different identifiers — a Behandelaar has no BSN, a Zorgverlener has no
rollen. This replaces the flatSessionthe day a second actor arrives. -
Authorization — "what may you do" → enforced at the backend / context boundary, where the backend is the authority (per ADR-0001). It is not a permission matrix living in
auth. The frontend receives only the decisions it needs to render (e.g. acanBeoordelenflag), exactly like every other server-owned rule.
4. "Other users" slot in without inventing contexts
Admin, auditor, institution-rep are additional Principal variants or additional rollen on
medewerker — never a new folder-per-role. A genuinely new bounded context is warranted only when
an actor brings a new language and capability (e.g. an "Toezicht/Handhaving" enforcement context),
not merely a new login.
Consequences
- This repo stays the pure SSP. No backoffice code leaks in; no role-named folders appear.
- The backoffice ships as a separate app against the same backend and the same OpenAPI contract.
- The one concrete FE change when actor #2 lands is
Session → Principalin theauthcontext; theauthGuard/SessionStoreseams already localise that (auth.guard.ts,session.store.ts). - The backend becomes the authority for the aanvraag status lifecycle and for authorization, publishing both as decision DTOs — a natural extension of ADR-0001, not a new pattern.
pendingHerregistratieis understood as a temporary stand-in for a real, backend-owned status.
Out of scope here (next steps, not built)
- Building the Behandeling backoffice application.
- Real authentication: DigiD (SSP) and employee SSO / eHerkenning (backoffice).
- The
authSession → Principalrefactor — deferred until a second actor is actually introduced. - The backend aanvraag status lifecycle + authorization endpoints/DTOs.
ponytail: this ADR draws the boundaries so nothing has to be undone later; it does not scaffold a
second app or a role system now. Introduce the Principal union and the status lifecycle when the
backoffice work actually starts — YAGNI until then.