Files
atomic-design-poc/docs/reference/architecture/0005-openzaak-behind-bff.md
T
ehoandClaude Opus 4.8 1c3c195d32
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
feat(backend): expand stamdata + OpenZaak-ready cases seam (WP-49)
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>
2026-07-24 15:01:06 +02:00

70 lines
4.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.