Stamdata: add beroepen, opleidingen (temporal), and specialismen tables to the schema-driven catalog (zero UI code). opleidingen.beroep and specialismen.beroep both reference beroepen.code — the first stamdata->stamdata references, enforced by two new StamdataRef entries in the CI gate. OpenZaak/ZGW (WP-49, slice 1 — read-only zaken): introduce IZaakSource as the cases read seam. Default LocalZaakSource reads the local SQLite store (offline); an OpenZaakZaakSource (Zgw/ client: HS256 per-call JWT, ZGW->existing-DTO mapper, paginating HTTP source) is selected behind Zgw:Enabled (default false). The FE never changes — same ApplicationSummaryDto, no api-client drift. Unit-tested with fixtures + a stub HttpMessageHandler; no live OpenZaak needed. Docs: ADR-0005, reference/openzaak-integration.md, WP-49..52 roadmap, stamdata.md update, README index rows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5.0 KiB
OpenZaak / ZGW integration — how the BFF connects (& how to extend)
How the BFF sources cases from a real OpenZaak (ZGW APIs) while the frontend stays unchanged. For the why, see ADR-0005; this page is how the seam is built and how to add the next slice. Built in WP-49 (read-only zaken).
The one rule: OpenZaak sits behind the BFF, never in the browser
The Angular app only ever sees the BFF's decision DTOs (BFF-lite, ADR-0001). All ZGW awkwardness — URL-as-identity, cross-service joins, JWT auth, pagination — is absorbed by the .NET BFF. Flipping the data source from local SQLite to OpenZaak is a backend config change with zero frontend change and no api-client drift.
The seam (data source by config)
Data/IZaakSource.cs— the cases READ interface. Returns the existingApplicationSummaryDto, so each implementation owns its own mapping.Data/LocalZaakSource.cs— default; reads the local SQLiteApplicationStore(offline, unchanged behaviour).Zgw/OpenZaakZaakSource.cs— the OpenZaak client; selected only whenZgw:Enabled=true.- Wiring (
Program.cs):if (Zgw:Enabled) AddHttpClient<IZaakSource, OpenZaakZaakSource>() else AddSingleton<IZaakSource, LocalZaakSource>(). The/admin/casesendpoint resolvesIZaakSourcefrom DI — routes + DTOs untouched.
The ZGW client (backend/src/BigRegister.Api/Zgw/)
ZgwOptions.cs— bound from theZgwappsettings section:Enabled, per-service base URLs (ZrcBaseUrl,ZtcBaseUrl),ClientId,Secret,UserId,UserRepresentation. The five ZGW APIs are separate base URLs; slice 1 needs only Zaken (ZRC) + Catalogi (ZTC).ZgwTokenProvider.cs— mints an HS256 JWT per call (iss/client_id/iat/user_id/user_representation). No refresh flow — OpenZaak expires tokens 1h pastiat, so per-call minting is the recommended pattern. Hand-rolled (noMicrosoft.IdentityModel.*dependency).ZgwZaakMapper.cs— the anti-corruption map: ZGW Zaak →ApplicationSummaryDto. This is where URL identity becomes the trailing uuid and the zaaktype URL is resolved to a human label (the cross-service join).OpenZaakZaakSource.cs— follows{count,next,previous,results}pagination, resolves + caches zaaktype labels, attachesAuthorization: Bearer <jwt>.
The five ZGW APIs (context for later slices)
| API | Component | Used by |
|---|---|---|
| Zaken | ZRC | slice 1 (read), WP-50 (create) |
| Catalogi | ZTC | slice 1 (zaaktype label; also type URLs for create) |
| Documenten | DRC | WP-51 (upload + zaak↔document link) |
| Besluiten | BRC | later (formal decisions) |
| Notificaties | NRC | WP-52 (live status via webhooks, not polling) |
How to add the next slice
- Read — extend
IZaakSource(or add a sibling interface, e.g.IDocumentSource) with the new operation; implement it on bothLocalZaakSourceand the OpenZaak source. Keep the return type the existing DTO so the FE never changes. - Write (create-zaak, WP-50) — a create needs a
zaaktypeURL from Catalogi (OpenZaak validates it by fetching), then usually a follow-upstatus+rol. Route it through the existing submit/mutation seam. - Enforce server-side for anything the FE gates — a config value the FE echoes is never the authority (ADR-0001).
Coupling
Low and one-directional. Consumer coupling is near zero — IZaakSource is injected at one
endpoint, and the FE is fully decoupled by the DTO. The producer side is contained in Zgw/:
add a slice by adding a source method + a mapper case, not by touching the FE or the contract.
Watch the sync-over-async ponytail: note in OpenZaakZaakSource — make the cases read
path async if OpenZaak becomes the default.
Config
// appsettings.json — off by default (POC runs offline on the local store)
"Zgw": {
"Enabled": true,
"ZrcBaseUrl": "https://open-zaak.example/zaken/api/v1",
"ZtcBaseUrl": "https://open-zaak.example/catalogi/api/v1",
"ClientId": "big-register", "Secret": "<from a secret store>",
"UserId": "<session user>", "UserRepresentation": "<session name>"
}
See also
- ADR-0005 — OpenZaak behind the BFF — the decision.
- ADR-0001 — BFF-lite + decision DTOs — why the FE doesn't change.
- WP-49 (this), WP-50/51/52 (later slices).
backend/src/BigRegister.Api/Zgw/— the client;Data/IZaakSource.cs— the seam.- ZGW standard (VNG) · OpenZaak auth docs.