feat(openzaak): real notification delivery to the BFF webhook (WP-58)

OpenZaak doesn't serve the Notificaties API itself (it's a separate app,
open-notificaties) — standing one up for a real abonnement would triple
this harness for a benefit it doesn't need (exactly one subscriber, this
repo's own BFF). Instead, an opt-in compose overlay adds a celery worker
and points OpenZaak's NotificationsConfig straight at the BFF's webhook
via a zgw_consumers Service; bootstrap-notificaties.sh configures it
idempotently and verify-notificatie.sh proves a real write delivers to
the BFF's audit trail end-to-end.

Verified live: preflight proves the webhook's shared-secret gate both
ways (204/401), a zaak PATCH triggers real celery delivery, and rerunning
both scripts against an already-configured harness stays idempotent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-30 15:33:16 +02:00
co-authored by Claude Sonnet 5
parent 1e87997ea0
commit 3e983bd2cc
8 changed files with 364 additions and 22 deletions
+63 -6
View File
@@ -132,9 +132,11 @@ to `IZaakSource` per call), so a valid notification's only visible effect right
row proving the round-trip works end-to-end. Add real invalidation at the `// ponytail:` marker
in `Program.cs` if a cache is ever introduced.
**Provisioning the `abonnement` is out-of-band, one-time config against a live OpenZaak — not
app code.** Register it once (e.g. via OpenZaak's admin UI or a `POST` to its Abonnementen API)
pointing at this BFF's public URL:
**A real deployment provisioning is out-of-band, one-time config against a live OpenZaak — not
app code.** OpenZaak does not serve the Notificaties API itself — it's a separate application
(`open-notificaties`, its own image/DB/celery stack). Register the `abonnement` once (e.g. via
Open Notificaties' admin UI or a `POST` to its Abonnementen API) pointing at this BFF's public
URL:
```jsonc
{
@@ -144,6 +146,13 @@ pointing at this BFF's public URL:
}
```
`auth` is sent verbatim as the `Authorization` header on every callback (the NRC's
`auth_type=api_key` default) — no `Bearer` prefix, matching this endpoint's plain string
compare.
**The dev harness (WP-58) skips the NRC entirely** — see "Notifications-enabled profile"
below.
## Identity — the acting citizen (WP-53)
Everything above used to hardcode a single owner (`DocumentStore.DemoOwner`) and a single static
@@ -224,6 +233,53 @@ enforces ZGW's geo-header requirement in a way no stub-based test could catch, s
never rejects an unexpected (or missing) header. That is the harness's whole point — proving
the seam against real protocol behaviour, not just the shapes we already assumed.
### Notifications-enabled profile (WP-58)
The base harness above runs with `NOTIFICATIONS_DISABLED: 'true'` (no celery worker) — fine for
proving the read/write ZGW seam, but it means a write to a notified resource never actually
delivers anything. `docker-compose.openzaak.notificaties.yml` is an opt-in overlay that adds the
one celery worker OpenZaak needs to deliver a notification, and flips that flag off. The two
changes are inseparable: the moment `NOTIFICATIONS_DISABLED` is false, OpenZaak's
`NotificationsConfig` must have a client configured or every write to a notified resource 500s
and rolls back (`NOTIFICATIONS_GUARANTEE_DELIVERY` defaults true) — so `bootstrap-notificaties.sh`
configures that client in the same step.
A real Notificaties API (NRC) is a separate application this harness doesn't stand up (see the
"Notificaties webhook" section above) — reproducing it here (its own DB + celery + a real
`abonnement`/kanaal registration) would roughly triple the harness for a benefit this dev loop
doesn't need: there's only ever one subscriber (this repo's own BFF). Instead
`bootstrap-notificaties.sh` points OpenZaak's `NotificationsConfig` straight at the BFF's webhook
via a `zgw_consumers.Service` (`auth_type=api_key`, so the configured secret is sent verbatim as
the `Authorization` header — exactly what the endpoint's plain string-compare expects). Same
delivery proof (`write → OpenZaak's celery worker → a real HTTP POST → the BFF's audit trail`),
far less to stand up and keep alive. A real deployment with more than one subscriber, or that
needs kanaal-filtered fan-out, needs a real NRC + `abonnement` — this harness's shortcut doesn't
model that.
The overlay's `celery` worker joins the repo root's own `docker compose up` network (by name,
`api`) to reach the BFF — `host.docker.internal:host-gateway` was tried first, but this
environment's rootless Podman doesn't route container→host-port traffic through it (DNS
resolves, every TCP connect times out); container-to-container is the reliable path regardless
of Docker vs. Podman. That means the notifications profile needs the repo root's `docker compose
up` (or an equivalent `api` container on that network) running too, with
`Zgw__NotificatieAuthorization` set:
```bash
docker compose run --rm -d --name atomic-design-poc-api-1 --service-ports \
-e Zgw__NotificatieAuthorization='<a secret>' api # repo root
cd backend/openzaak
docker compose -f docker-compose.openzaak.yml -f docker-compose.openzaak.notificaties.yml up -d
./bootstrap-catalogus.sh
BFF_AUTH='<the same secret>' ./bootstrap-notificaties.sh
BFF_AUTH='<the same secret>' ./verify-notificatie.sh # proves a real delivery, end to end
```
Verified live in-session: the preflight in `bootstrap-notificaties.sh` proved the BFF's auth gate
both ways (204 with the secret, 401 without/wrong), `verify-notificatie.sh` found the delivered
`zgw:notificatie`/`allow` audit row for the PATCHed zaak, and re-running both scripts against the
already-configured client stayed idempotent (no errors, no duplicate `Service` rows).
## Config
```jsonc
@@ -247,9 +303,10 @@ the seam against real protocol behaviour, not just the shapes we already assumed
"identiteit": "https://open-zaak.example/catalogi/api/v1/informatieobjecttypen/<uuid>",
"diploma": "https://open-zaak.example/catalogi/api/v1/informatieobjecttypen/<uuid>"
},
// WP-52 (Notificaties): NRC base URL (documentation/provisioning only, no outbound call) +
// the shared secret NRC must send back on every webhook POST.
"NrcBaseUrl": "https://open-zaak.example/notificaties/api/v1",
// WP-52 (Notificaties): NRC base URL — a SEPARATE host/app from OpenZaak itself
// (documentation/provisioning only, no outbound call) + the shared secret NRC must send
// back on every webhook POST.
"NrcBaseUrl": "https://open-notificaties.example/api/v1",
"NotificatieAuthorization": "<same value registered in the abonnement's `auth` field>"
}
```