# OpenZaak / ZGW integration — how the BFF connects (& how to extend) How the BFF sources cases from a real **OpenZaak** (ZGW APIs) while the frontend stays unchanged. For the _why_, see [ADR-0005](architecture/0005-openzaak-behind-bff.md); this page is _how the seam is built and how to add the next slice_. Built in [WP-49](../project/backlog/WP-49-openzaak-zaken-read-seam.md) (read-only zaken). ## The one rule: OpenZaak sits behind the BFF, never in the browser The Angular app only ever sees the BFF's decision DTOs (BFF-lite, ADR-0001). All ZGW awkwardness — URL-as-identity, cross-service joins, JWT auth, pagination — is absorbed by the .NET BFF. Flipping the data source from local SQLite to OpenZaak is a **backend config change** with **zero frontend change and no api-client drift**. ## The seam (data source by config) - `Data/IZaakSource.cs` — the cases READ interface. Returns the existing `ApplicationSummaryDto`, so each implementation owns its own mapping. - `Data/LocalZaakSource.cs` — **default**; reads the local SQLite `ApplicationStore` (offline, unchanged behaviour). - `Zgw/OpenZaakZaakSource.cs` — the OpenZaak client; selected only when `Zgw:Enabled=true`. - Wiring (`Program.cs`): `if (Zgw:Enabled) AddHttpClient() else AddSingleton()`. The `/admin/cases` endpoint resolves `IZaakSource` from DI — routes + DTOs untouched. ## The ZGW client (`backend/src/BigRegister.Api/Zgw/`) - `ZgwOptions.cs` — bound from the `Zgw` appsettings section: `Enabled`, per-service base URLs (`ZrcBaseUrl`, `ZtcBaseUrl`), `ClientId`, `Secret`, `UserId`, `UserRepresentation`. The five ZGW APIs are separate base URLs; slice 1 needs only Zaken (ZRC) + Catalogi (ZTC). - `ZgwTokenProvider.cs` — mints an **HS256 JWT per call** (`iss`/`client_id`/`iat`/`user_id`/ `user_representation`). No refresh flow — OpenZaak expires tokens 1h past `iat`, so per-call minting is the recommended pattern. Hand-rolled (no `Microsoft.IdentityModel.*` dependency). - `ZgwZaakMapper.cs` — the anti-corruption map: ZGW Zaak → `ApplicationSummaryDto`. This is where **URL identity** becomes the trailing uuid and the **zaaktype URL** is resolved to a human label (the cross-service join). - `OpenZaakZaakSource.cs` — follows `{count,next,previous,results}` pagination, resolves + caches zaaktype labels, attaches `Authorization: Bearer `. ## The five ZGW APIs (context for later slices) | API | Component | Used by | | ------------ | --------- | --------------------------------------------------- | | Zaken | ZRC | slice 1 (read), WP-50 (create) | | Catalogi | ZTC | slice 1 (zaaktype label; also type URLs for create) | | Documenten | DRC | WP-51 (upload + zaak↔document link) | | Besluiten | BRC | later (formal decisions) | | Notificaties | NRC | WP-52 (live status via webhooks, not polling) | ## How to add the next slice 1. **Read** — extend `IZaakSource` (or add a sibling interface, e.g. `IDocumentSource`) with the new operation; implement it on both `LocalZaakSource` and the OpenZaak source. Keep the return type the existing DTO so the FE never changes. 2. **Write** (create-zaak, WP-50) — a create needs a `zaaktype` URL from Catalogi (OpenZaak validates it by fetching), then usually a follow-up `status` + `rol`. Route it through the existing submit/mutation seam. 3. **Enforce server-side** for anything the FE gates — a config value the FE echoes is never the authority (ADR-0001). ## Coupling Low and one-directional. Consumer coupling is near zero — `IZaakSource` is injected at one endpoint, and the FE is fully decoupled by the DTO. The producer side is contained in `Zgw/`: add a slice by adding a source method + a mapper case, not by touching the FE or the contract. Watch the **sync-over-async** `ponytail:` note in `OpenZaakZaakSource` — make the cases read path async if OpenZaak becomes the default. ## Config ```jsonc // appsettings.json — off by default (POC runs offline on the local store) "Zgw": { "Enabled": true, "ZrcBaseUrl": "https://open-zaak.example/zaken/api/v1", "ZtcBaseUrl": "https://open-zaak.example/catalogi/api/v1", "ClientId": "big-register", "Secret": "", "UserId": "", "UserRepresentation": "" } ``` ## Anti-corruption layer — two nested boundaries (what to learn) This setup is an anti-corruption layer (ACL) **twice over**, and seeing them as a pair is the lesson worth taking away: 1. **The BFF guards everything against upstream systems.** OpenZaak's foreign model — URL-as-identity, a `zaaktype` that is a URL _into another service_, `{count,next,previous, results}` pagination, HS256 JWT auth — never leaves the BFF. `ZgwZaakMapper` translates it into the BFF's own `ApplicationSummaryDto`; `IZaakSource` makes the boundary swappable (`LocalZaakSource` vs `OpenZaakZaakSource` return the _same_ DTO). 2. **The Angular app guards itself against the BFF.** `infrastructure/` is the only layer that touches the network (lint-enforced); every response crosses a `parse*` (`Result`) trust boundary + a `toDomain` mapper before any domain/UI code sees it (ADR-0001, ARCHITECTURE §6). The DTO at `/api/v1` is the membrane between them — which is why wiring OpenZaak touched **zero frontend code and produced zero api-client drift**. That was the proof the ACL held. Principles this demonstrates: - **An ACL is a _mapping_, not a passthrough.** A DTO that is the upstream shape renamed is corruption with extra steps; the valuable ACLs here (`ZgwZaakMapper`, the `parse*`/`toDomain` pairs) actively translate a foreign model into a local one. - **Put the ACL where trust changes, and make it the _only_ place.** One choke point per boundary — the `Zgw/` folder + `IZaakSource` server-side, `infrastructure/` client-side. - **Decision DTOs and the ACL are complementary.** BFF-lite (server decides, FE renders) is an ACL against _business-rule_ drift, layered on the ACL against _data-shape_ drift. - **A real seam is swap-testable offline.** Because the ACL returns a stable DTO, the ZGW client is unit-testable with fixtures + a stub handler — no live OpenZaak. - **Mark the honest edges.** The `ponytail:` sync-over-async note and the "coarse status map" comment in `ZgwZaakMapper` show where the ACL is deliberately thin — an ACL need not be complete on day one, but its shortcuts should be visible. Caveat: today only the cases **read** path has a source interface (`IZaakSource`). Other BFF endpoints still read `SeedData`/static stores directly — ACL-ready (the DTO seam exists) but not yet swappable. That is the WP-50/51/52 roadmap. ## See also - [ADR-0005 — OpenZaak behind the BFF](architecture/0005-openzaak-behind-bff.md) — the decision. - [ADR-0001 — BFF-lite + decision DTOs](architecture/0001-bff-lite-decision-dtos.md) — why the FE doesn't change. - [WP-49](../project/backlog/WP-49-openzaak-zaken-read-seam.md) (this), WP-50/51/52 (later slices). - `backend/src/BigRegister.Api/Zgw/` — the client; `Data/IZaakSource.cs` — the seam. - [ZGW standard (VNG)](https://vng-realisatie.github.io/gemma-zaken/) · [OpenZaak auth docs](https://open-zaak.readthedocs.io/en/stable/client-development/authentication.html).