CI / frontend (push) Successful in 2m59s
CI / backend (push) Successful in 1m27s
CI / semgrep (push) Successful in 58s
CI / e2e (push) Successful in 2m30s
CI / api-client-drift (push) Canceled after 1m14s
CI / storybook-a11y (push) Canceled after 29m8s
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>
70 lines
4.0 KiB
Markdown
70 lines
4.0 KiB
Markdown
# ADR-0005 — OpenZaak (ZGW APIs) behind the BFF
|
||
|
||
Status: Accepted · Date: 2026-07-24
|
||
|
||
## Context
|
||
|
||
The POC serves cases (aanvragen) from a local SQLite store. To grow toward production it must
|
||
be able to source cases from a real Dutch **Zaakgericht Werken (ZGW)** backend — **OpenZaak**,
|
||
the VNG reference implementation. ZGW is not one API but five separate services (Zaken/ZRC,
|
||
Documenten/DRC, Catalogi/ZTC, Besluiten/BRC, Notificaties/NRC), each on its own base URL, with
|
||
traits that make raw responses unfit to hand to a browser:
|
||
|
||
- resources are identified by **full URLs**, not bare ids;
|
||
- references between resources are **URLs into other services** (a zaak's `zaaktype` lives in
|
||
Catalogi), so a single screen means joining across services;
|
||
- lists use a uniform `{count,next,previous,results}` pagination envelope;
|
||
- auth is a short-lived **HS256 JWT** signed with a client secret (no OAuth refresh), which
|
||
OpenZaak rejects an hour past `iat`.
|
||
|
||
Two constraints shaped the decision: the **frontend must not change** (BFF-lite, ADR-0001 —
|
||
the FE renders decision DTOs and never recomputes rules), and the POC must **still run fully
|
||
offline** (no OpenZaak needed for local dev/CI).
|
||
|
||
The backend, however, had **no data-access abstraction** — endpoints called concrete static
|
||
stores directly — and no outbound HTTP or JWT machinery. So there was no injection point to
|
||
swap a data source behind.
|
||
|
||
## Options
|
||
|
||
1. **FE talks to OpenZaak directly.** Rejected: leaks ZGW shapes + the client secret to the
|
||
browser, and contradicts BFF-lite.
|
||
2. **Rewrite the static stores in place to call OpenZaak.** Rejected: no seam, no offline mode,
|
||
all-or-nothing, untestable without a live server.
|
||
3. **Introduce a data-source interface behind the existing DTO contract, select the
|
||
implementation by config.** Chosen.
|
||
|
||
## Decision
|
||
|
||
Put the OpenZaak anti-corruption layer **in the .NET BFF**, never in the browser. Introduce a
|
||
per-domain source interface (starting with `IZaakSource` for the cases read path) whose default
|
||
implementation reads the local SQLite store and whose alternate implementation calls OpenZaak —
|
||
selected by a config flag (`Zgw:Enabled`, default false). Each implementation maps into the
|
||
**existing** wire DTO (`ApplicationSummaryDto`), so the `/api/v1` contract and the FE are
|
||
untouched. The BFF holds the client secret and **mints a fresh JWT per outbound call**.
|
||
|
||
This is deliberately a **thin vertical slice** (read-only zaken, WP-49); create/documents/
|
||
notifications follow the same seam in later slices (WP-50/51/52) rather than being scaffolded
|
||
up front — the migration stance ADR-0001 already prescribes.
|
||
|
||
## Consequences
|
||
|
||
- **+** The FE is production-ready as-is: swapping to OpenZaak is backend-only, behind one
|
||
config flag, with zero DTO/api-client drift. The POC still runs offline (default = local).
|
||
- **+** The seam is unit-testable without a live server: the JWT minter, the ZGW→DTO mapper,
|
||
and the paginating source are all covered with fixtures + a stub `HttpMessageHandler`.
|
||
- **+** URL-as-identity and cross-service joins are contained in one mapper; nothing downstream
|
||
sees a ZGW shape.
|
||
- **−** Only the cases **read** path has a source interface today; other endpoints still call
|
||
static stores directly. Each future slice introduces its own seam as needed (not a big-bang
|
||
repository refactor).
|
||
- **−** `IZaakSource` is synchronous (matching the existing sync endpoint + local store), so
|
||
`OpenZaakZaakSource` does sync-over-async; fine under ASP.NET Core (no sync-context), to be
|
||
made async if OpenZaak becomes the default. Marked with a `ponytail:` note at the call site.
|
||
- **Shipped with this ADR (WP-49):** `IZaakSource` + `LocalZaakSource` (default) +
|
||
`OpenZaakZaakSource` (config-gated), the `Zgw/` client (`ZgwOptions`, `ZgwTokenProvider`,
|
||
`ZgwZaakMapper`), and the reference guide [openzaak-integration.md](../openzaak-integration.md).
|
||
- **Deferred:** real inbound OIDC/JWT auth (still header-stubbed), create-zaak (WP-50),
|
||
Documenten/DRC upload + link (WP-51), Notificaties/NRC webhooks (WP-52), adding OpenZaak to
|
||
docker-compose.
|