# 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.