## What & why S-15a, the first of the S-15 (#16) split. A new **beheer** portal (medewerker realm, like behandel) shows the ZTC catalogus — the published zaaktypen — **read-only**. A beheerder logs in and sees the seeded BIG-REGISTRATIE zaaktype. Closes #130 ### The vertical portal → BFF `GET /beheer/catalogi/zaaktypen` (medewerker realm + `beheerder` role) → ACL `GET /catalogi/zaaktypen` → ZGW Catalogi API. - **ACL**: new read-only `GET /catalogi/zaaktypen` listing published zaaktypen (reuses the ADR-0021 Catalogi client; public-safe `identificatie`/`omschrijving`). - **BFF**: new typed `IAclClient` + `Downstream:Acl:BaseUrl`, and `GET /beheer/catalogi/zaaktypen` behind a new `beheerder` policy (reuses the medewerker bearer scheme + realm-role lifting). OpenAPI spec + generated Angular client regenerated. - **Keycloak**: `beheerder` realm role + `bram-beheerder` test user in the medewerker realm. - **Frontend**: new `apps/beheer` Angular app (copied from behandel) with a read-only catalogus page; `SECURE_API_ROUTES=['/beheer/']`. - **Infra**: `beheer` compose service (port 8143), added to `WAIT_SVCS` + CI log-dump; a Playwright e2e (beheerder login → catalogus shows BIG-REGISTRATIE). ### New boundary → ADR-0025 The BFF now reaches the **ACL directly** for the catalogus read — a new service-to-service edge (§14). The catalogus is neither a domain nor a projection concern, and §8.1 means only the ACL may read ZGW; routing through the domain would pollute it with a non-domain passthrough. §8.1/§8.3 stay intact. Recorded in **ADR-0025**. ## Definition of Done - [x] Failing test committed before each implementation (red→green per layer: ACL, BFF, frontend). - [x] Conventional Commits referencing #130. - [ ] CI green — pending Gitea Actions run. - [x] `docker compose up` brings up `beheer` (health-gated in `WAIT_SVCS`). - [x] Docs — ADR-0025 + demo-script S-15a note. - [x] Demo note in `docs/demo-script.md`. ## Verified locally lint (`dotnet format`) ✓ · .NET unit (Acl 57 / Big 152 / EventSubscriber 19 / Bff 40) ✓ · frontend lint+test (8 projects) ✓ · frontend build (4 apps) ✓. Mutation ratchet: added a gateway unit test for the new `ListZaaktypenAsync` mapping so the ACL score holds. verify-stack (compose smoke + e2e) runs in CI. ## Notes for reviewers - The BFF drops the ZGW URL from `BeheerZaaktype` (public-safe: identificatie + omschrijving only). - The catalogus e2e asserts on the stable seeded `BIG-REGISTRATIE` (not a per-test reference), safe on the shared verify stack. - Follow-ups: **S-15b** (#131) default-fill CRUD, **S-15c** (#132) medewerker-realm MFA. 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #133
2.4 KiB
ADR-0025: The BFF reads the catalogus directly from the ACL
- Status: Accepted
- Date: 2026-07-24
- Deciders: Respellion engineering
- Slice: S-15a (#130), first of the S-15 (#16) split
Context
The beheer portal shows a read-only view of the ZTC catalogus (the published zaaktypen). Two coupling rules constrain where that data can come from:
- §8.1 — only the ACL may talk to the ZGW APIs (Catalogi included). So the catalogus read must originate in the ACL.
- §8.3 — portals talk only to the BFF. So the portal reaches the ACL only through the BFF.
That leaves the question of how the BFF gets the data. Until now the BFF fanned out to exactly two backends — the Domain Service and the read projection. The catalogus is neither: it is not a registration (domain) nor a projected read model.
Decision
The BFF calls the ACL directly for the beheer catalogus read — a new typed
IAclClient (GET /catalogi/zaaktypen), configured by Downstream:Acl:BaseUrl,
mirroring the existing IDomainClient / IProjectionClient pattern.
Rejected alternative — route it through the Domain Service (BFF → domain → ACL): the catalogus is not a domain concern, so the domain would gain a pass-through endpoint that owns no aggregate and no invariant, blurring the domain's responsibility purely to avoid a new edge. That is worse coupling, not better.
This adds one service-to-service edge (BFF → ACL) — an architecturally
significant boundary change (§14), hence this ADR. It does not bend §8: the
ACL stays the only code that reads ZGW, and the portal still talks only to the
BFF. The ACL endpoint is a plain read that trusts its callers (§8.3); the
beheerder authorization lives at the BFF (medewerker realm + beheerder role).
Consequences
Positive
- The catalogus read follows the shortest honest path; the domain stays about registrations.
- Symmetric with the other downstream clients — nothing new to learn.
Negative / costs
- The BFF now depends on three backends instead of two. The ACL must be reachable for the beheer portal to load (it already is — the BFF is on the same network).
- A second consumer of the ACL (alongside the domain and event-subscriber), so ACL read endpoints are now part of more than one caller's contract.
Coupling rules touched (CLAUDE.md §8)
A new BFF → ACL edge. §8.1 and §8.3 remain intact; §14 (boundary change) is the reason this ADR exists.