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

72 lines
4.3 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).
- **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.