Files
atomic-design-poc/docs/reference/architecture/0005-openzaak-behind-bff.md
T
ehoandClaude Sonnet 5 de3bff0d7f feat(zgw): OpenZaak create-zaak, first write slice (WP-50)
Extends the IZaakSource seam (WP-49, read-only) with CreateZaak: submitting
an aanvraag now also registers a Zaak + Status + Rol in OpenZaak when
Zgw:Enabled=true, routed through the existing /applications/{id}/submit
endpoint with the FE response DTO unchanged (ADR-0001/ADR-0005 — the
endpoint never branches on the config flag itself, DI already picked the
implementation).

- ZgwOptions gains a Type→zaaktype-URL map + the two RSINs a Zaak needs.
- LocalZaakSource.CreateZaak is a pure passthrough of what the endpoint
  already computes locally (zero behaviour change for the offline default).
- OpenZaakZaakSource.CreateZaak POSTs the zaak (identificatie = the same
  local reference, so both stay in sync), resolves + POSTs the initial
  status and the initiator rol (BSN) via Catalogi lookups, and maps the
  result back into the submit response.
- Marked ponytail shortcuts: first-statustype/roltype-Catalogi-returns
  (no per-type config) and no compensating transaction on partial failure
  — both fine for a first slice against a demo backend.

Verified: full `npm run ci` green, zero api-client drift, 144/144 backend
tests (142 existing + 2 new stub-handler tests asserting the POST bodies
+ type→zaaktype mapping per the acceptance criteria).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 09:03:13 +02:00

4.3 KiB
Raw Blame History

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.
  • Also shipped (WP-50): IZaakSource.CreateZaak — the first write. Submitting an aanvraag now also creates a Zaak + Status + Rol in OpenZaak when Zgw:Enabled=true, routed through the existing submit endpoint with zero DTO change (same seam, same anti-corruption boundary).
  • Deferred: real inbound OIDC/JWT auth (still header-stubbed), Documenten/DRC upload + link (WP-51), Notificaties/NRC webhooks (WP-52), adding OpenZaak to docker-compose.