Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
15 KiB
Demo script
A running log of demoable outcomes, one section per slice. Each entry is a short,
copy-pasteable walkthrough against a local make up stack.
S-08d — Walking skeleton complete: browser → submit, end-to-end
Outcome: the self-service portal is served in the stack and the full front-of-house happy path runs in a real browser — mock DigiD login → submit → confirmation — closing the walking skeleton (portal → BFF → domain → Flowable → ACL → OpenZaak, with the openbaar register reading the projection).
# 1. Bring the whole stack up (portal served on :8140, BFF :8080, Keycloak :8180).
make up
# 2. Automated happy path — Playwright, inside the compose network (issuer-consistent):
make verify-e2e # → login as jan-burger → submit → "ontvangen" confirmation
# 3. By hand: open the portal, log in as jan-burger / test123, click "Registratie indienen".
open http://localhost:8140
The portal is served same-origin with the BFF (nginx proxies
/self-service+/openbaar), so no CORS; the OIDC authority comes from/config.jsonat runtime. Seedocs/frontend-decisions.md.
S-08c — Self-service submit form (NL Design System + DigiD)
Outcome: a zorgprofessional logs in via mock DigiD and submits a BIG registration through the self-service portal (NL Design System styling); the page confirms with the reference returned by the BFF. The bsn comes from the DigiD token, so it's a confirm-and-submit flow (no bsn field).
# 1. Bring the backend + Keycloak up (BFF on :8080, Keycloak on :8180).
make up
# 2. Serve the portal (dev server); it redirects to Keycloak for DigiD login.
pnpm nx serve self-service # → http://localhost:4200
# 3. In the browser: log in as the mock DigiD user jan-burger / test123, then submit.
# The page shows the returned registration reference.
First real UI. The full login → submit → success happy path is automated in S-08d (Playwright, against the compose-served app). Component tests + an axe WCAG 2.1 AA check on the submit page run headless in the
frontendCI lane. Seedocs/frontend-decisions.md.
S-08a — Nx workspace + self-service portal skeleton
Outcome: the frontend foundation — an Nx (pnpm) monorepo with the self-service Angular app
(standalone + signals), lint/test/build green in a CI Node lane. The login + submit form follow in
S-08c.
# From a fresh clone (Node 24 + pnpm 11):
pnpm install # native builds are pre-approved in pnpm-workspace.yaml
pnpm nx test self-service # Vitest component test
pnpm nx build self-service # production build
pnpm nx serve self-service # → http://localhost:4200 (placeholder page)
# Or the CI-equivalent one-shot:
make frontend # install + nx lint/test/build
Nx manages only
apps/+libs/; the .NET services stay ondotnet/the Makefile. NL Design System and the real form arrive in S-08c (#67); seedocs/frontend-decisions.md.
S-07 — BFF: the portals' single backend
Outcome: the BFF validates Keycloak digid tokens on the self-service submit (forwarding the
bsn to the domain) and serves the openbaar register anonymously with only public-safe fields — the
front door the portals (S-08/S-09) will talk to.
The path: portal → BFF POST /self-service/registrations (token-gated) → domain; and
BFF GET /openbaar/register (anonymous) → projection-api. See ADR-0010.
# 1. Bring the full stack up.
make up
# 2. Drive the BFF end-to-end (401 without a token, 202 with a real digid token, anonymous openbaar).
make verify-bff # → "OK — BFF: 401 without token, 202 with a digid token, anonymous ..."
# 3. Try it by hand (BFF on host port 8080).
# a) A digid access token for the mock user jan-burger (bsn 123456782):
tok=$(curl -s -X POST http://localhost:8180/realms/digid/protocol/openid-connect/token \
-d grant_type=password -d client_id=big-portal -d username=jan-burger -d password=test123 \
| python3 -c "import sys,json;print(json.load(sys.stdin)['access_token'])")
# b) Submit — without the token it is 401; with it, 202:
curl -s -o /dev/null -w "no token -> %{http_code}\n" -X POST http://localhost:8080/self-service/registrations
curl -s -o /dev/null -w "with token-> %{http_code}\n" -X POST http://localhost:8080/self-service/registrations \
-H "Authorization: Bearer $tok"
# c) The openbaar register is anonymous and exposes only id + status (never the bsn):
curl -fsS http://localhost:8080/openbaar/register | jq
The self-service token is validated against Keycloak's
digidrealm; the openbaar lookup needs no token (S-09). The generated contract lives atservices/bff/openapi.json— S-08's client is built from it.
S-05 — BIG Domain Service: submit a registration
Outcome: submitting a registration starts a Flowable process; the external-task worker opens a zaak via the ACL and records it on the aggregate — the upstream half of the skeleton that produces the zaak S-06 then projects.
The path: domain POST /registrations → Flowable registratie process → OpenZaakAanmaken
worker → ACL → OpenZaak; GET /registrations/{id} shows the opened zaak (ADR-0009).
# 1. Bring the full stack up (seeds config, builds our services, waits for health).
make up
# 2. Drive the full path end-to-end. This also seeds a published BIG zaaktype and points the
# ACL at it (the zaak's zaaktype URL is server-assigned, so it isn't known at bring-up).
make verify-domain # → "OK — the domain opened a zaak and recorded it on the registration"
# 3. Submit one yourself (domain on host port 8130). Returns 202 + a Location to read back.
loc=$(curl -fsS -D - -o /dev/null -X POST http://localhost:8130/registrations \
-H 'Content-Type: application/json' -d '{"bsn":"123456782"}' | sed -n 's/\r$//; s/^[Ll]ocation: //p')
# 4. The worker opens the zaak off the request path (eventual consistency, ADR-0009); poll
# until zaakUrl is filled. (Step 2 must have run first, so the ACL knows the zaaktype.)
curl -fsS "http://localhost:8130$loc" | jq
# → { "registrationId": "...", "status": "Ingediend", "zaakUrl": "http://.../zaken/api/v1/zaken/<uuid>" }
Registration state is in-memory for this slice (ADR-0009); the rebuildable read model is the projection (S-06), fed by the very zaak this flow opens.
S-06 — Event Subscriber + read projection
Outcome: a zaak created in OpenZaak flows through NRC to the Event Subscriber, which projects it into a rebuildable read projection the projection-api serves.
The path: OpenZaak → (notification) NRC → (abonnement callback) Event Subscriber →
register_projection → projection-api GET /register.
# 1. Bring the full stack up (seeds config, builds our services, waits for health).
make up
# 2. Register the Event Subscriber's abonnement and create a zaak, then read it back.
# (The verify-projection check does exactly this end-to-end and asserts the result.)
make verify-projection # → "OK — projection-api serves zaak <uuid> with status INGEDIEND"
# 3. Observe the projection directly via the read API (host port 8120).
curl -fsS http://localhost:8120/register | jq
# → [ { "id": "<zaak-uuid>", "status": "INGEDIEND", "bsn": null, "naamPlaceholder": null } ]
# 4. Idempotency + rebuild: replays don't duplicate; a rebuild repopulates from the
# notification log (no OpenZaak access needed — ADR-0008).
curl -fsS -X POST http://localhost:8110/admin/rebuild # Event Subscriber, host port 8110
curl -fsS http://localhost:8120/register | jq 'length' # → unchanged
bsn/naam_placeholderare deferred (ADR-0008) — the notification doesn't carry them and the subscriber may not read OpenZaak directly (§8.1). They surface in a later slice.
S-09 — Openbaar Register portal (public visibility)
Outcome: the entry a zorgprofessional submits via self-service becomes publicly visible in the anonymous openbaar register portal — closing the walking-skeleton loop (submit → process → projection → public visibility).
The path: self-service submit → BFF → domain → (zaak) OpenZaak → NRC → Event Subscriber →
projection → openbaar portal reads the BFF's public-safe GET /openbaar/register.
# 1. Bring the full stack up (self-service :8140, openbaar :8141).
make up
# 2. Submit a registration via the self-service portal (mock DigiD: jan-burger / test123),
# or drive the whole happy path automatically (login → submit → public visibility):
make verify-e2e
# 3. Open the public register — no login. It lists the submitted entry (id + status only).
# Only public-safe fields cross the BFF: bsn / naam never appear.
open http://localhost:8141/ # search box; searches the BFF by referentie
curl -fsS http://localhost:8140/openbaar/register | jq # same public-safe view via the BFF proxy
# → [ { "id": "<zaak-uuid>", "status": "INGEDIEND" } ]
The register shows
INGEDIENDon submit; approval flips it toINGESCHREVEN— see S-09b below.
S-09b — Approval flow (public visibility flips to INGESCHREVEN)
Outcome: a behandelaar approves a submitted registration via a temporary admin endpoint (no
behandel-portal yet — S-12). The approval sets the zaak's final status through the ACL, which flows
back to the projection over NRC, and the openbaar register then shows the entry as INGESCHREVEN.
The path: POST /registrations/{id}/approve (domain) → ACL sets the zaak eindstatus (ZGW
/statussen) → OpenZaak → NRC → Event Subscriber projects INGESCHREVEN → openbaar register.
# 1. Full stack up, then drive submit → public INGEDIEND → approve → public INGESCHREVEN:
make up
make verify-e2e
# 2. Or by hand: submit (as in S-09), note the reference, then approve it.
# The zaak is opened off the request path, so approve once GET shows a zaakUrl.
ref="<registration-reference-from-the-confirmation>"
curl -fsS http://localhost:8130/registrations/$ref | jq # domain (host port 8130): wait for .zaakUrl
curl -fsS -X POST http://localhost:8130/registrations/$ref/approve -i # → 204 No Content
# 3. The public register now shows the entry as approved.
curl -fsS http://localhost:8140/openbaar/register | jq
# → [ { "id": "<zaak-uuid>", "status": "INGESCHREVEN" } ]
End of walking skeleton (S-09 + S-09b): submit → process → projection → public visibility, from INGEDIEND through approval to INGESCHREVEN. The subscriber takes any post-creation status-set as the approval (ADR-0011) — a walking-skeleton assumption that tightens when more transitions arrive (S-12+).
#78 — One reference across both portals (ADR-0012)
Before this change the self-service confirmation and the openbaar register showed different
identifiers, so a citizen could not look their registration back up. Now both show the same
reference: the domain registrationId is set as the zaak's identificatie by the ACL, and the
Event Subscriber enriches the projection with it by reading the zaak through the ACL (§8.1) — storing
it in the replay log so rebuild stays log-only (ADR-0008).
The path: domain passes registrationId → ACL sets it as zaak.identificatie → NRC →
Event Subscriber asks the ACL for the reference → projection row + replay log → openbaar register.
# Submit as in S-09 and note the reference on the confirmation, then find it in the public register:
ref="<registration-reference-from-the-confirmation>"
curl -fsS "http://localhost:8140/openbaar/register?q=$ref" | jq
# → [ { "id": "<zaak-uuid>", "status": "INGEDIEND", "reference": "<same-ref-as-confirmation>" } ]
The openbaar register's "Referentie" column and its search now use this reference — the exact value the citizen saw on submit. Asserted end-to-end by the Playwright happy path.
S-12 — Behandel portal: werkbak + beoordeling (#13, ADR-0013)
A behandelaar now works submitted registrations in a real portal instead of the temporary admin
endpoint. After a citizen submits (as above), the workflow parks the registration at the Flowable
Beoordelen user task, and it shows up in the werkbak. The behandelaar logs in against the
Keycloak medewerker realm and decides — goedkeuren (→ INGESCHREVEN via the ACL, per ADR-0011)
or afwijzen — which also completes the Beoordelen task so the process advances.
# 1. Open the behandel portal and log in as a behandelaar (medewerker realm):
# http://localhost:8142/ → merel-behandelaar / test123
#
# 2. The werkbak lists the registrations awaiting beoordeling (referentie / bsn / status).
# Find the reference from the submit confirmation and click "Goedkeuren" on that row.
#
# 3. The row drops off the werkbak (its Beoordelen task is completed) and the openbaar register
# (http://localhost:8141/) now shows that reference as INGESCHREVEN.
The path: behandel portal → BFF POST /behandel/registrations/{id}/decide (behandelaar policy,
medewerker realm) → domain applies the decision + completes the Flowable Beoordelen task →
ACL → NRC → event-subscriber → projection → openbaar register shows INGESCHREVEN.
The full round-trip — DigiD submit → public INGEDIEND → behandelaar goedkeurt in the werkbak → public INGESCHREVEN — is the Playwright happy path (
tests/e2e/registration.spec.ts), which now drives the behandel portal in place of the old admin endpoint.
S-11 — Withdrawal: "trek aanvraag in" (#12, ADR-0014)
A zorgprofessional can withdraw their own still-open registration from the self-service portal. The withdrawal is owner-scoped (the BFF forwards the DigiD token's bsn; the domain only lets the owner withdraw) and cancels the running workflow via a BPMN message event, so the case leaves the behandelaar's werkbak.
# 1. Log in and submit at the self-service portal (http://localhost:8140/, jan-burger / test123),
# note the "Referentie" on the confirmation.
# 2. Click "Trek aanvraag in" → the page confirms the registration is ingetrokken.
# 3. In the behandel werkbak (http://localhost:8142/, merel-behandelaar) the registration no longer
# appears — its Beoordelen task was cancelled.
The path: self-service → BFF POST /self-service/registrations/{id}/withdraw (DigiD, owner-scoped)
→ domain sets INGETROKKEN + correlates the RegistratieIngetrokken message to the process → the
interrupting boundary event ends it → the werkbak drops the case.
DigiD submit → trek aanvraag in → ingetrokken is the Playwright happy path (
tests/e2e/withdrawal.spec.ts); the owner-scoping + workflow cancellation are covered by theEen registratie intrekkenacceptance scenarios and the domain live check.