Two backlog trees are complete: `docs/project/backlog/` (75 files, every WP done) and `docs/project/refactor-backlog-setup/` (the arc before it). Move both under `docs/project/archive/` with `git mv`, so history stays intact through `git log --follow`. `SHOWCASE-ROADMAP.md` moves with them, because it points at the now-archived backlog README. Add `docs/project/archive/README.md`. It states that these trees are historical and names the two directories that are still live. Repoint every inbound reference named in RD-30's Files table: CLAUDE.md, the root README, both backend READMEs, `LetterHtml.cs`, `a11y.mdx`, the `document-feature` and `new-ssp` skills, and the readable-codebase PLAN, README, and RD-19 ticket. Fix two upward-relative links inside the moved WP files (WP-68, WP-69) that gained a directory level and would otherwise break. Repoint `.prettierignore`'s two agent-prompt exclusions to their new path, so prettier keeps leaving those files' exact wording alone. Mark RD-30 done and check off its acceptance criteria; flip its README row to done. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.9 KiB
WP-58 — Real notifications (celery + scripted abonnement)
Status: done Phase: 10 — OpenZaak production hardening
Why
The WP-54 harness deliberately trims celery/celery-beat/celery-flower and nginx, and sets
NOTIFICATIONS_DISABLED: 'true' — without a Celery worker, OpenZaak 500s and rolls back on
every write to a notified resource. Fine for a fixture-driven integration harness; a real
deployment that wants live Notificaties (WP-52's webhook) needs the workers running and the
abonnement (subscription) actually registered against the BFF's public callback URL —
today that registration step is manual.
Read first
backend/openzaak/docker-compose.openzaak.yml(top-of-file ponytail note)- openzaak-integration.md (Notificaties section)
- WP-52
Decisions (pre-made, don't relitigate)
- Add celery + celery-beat as additional compose services (same
openzaak/open-zaakimage, different command), pointed at the same redis broker already in the harness. - Registering the
abonnementbecomes a scripted, idempotent step — not a manual admin-UI action — parameterized by the BFF's real public URL. - Keep the existing WP-54 harness variant (
NOTIFICATIONS_DISABLED: 'true') available for fast local iteration where a live webhook round-trip isn't needed; this WP is additive (a "with notifications" profile/override), not a replacement.
Files
backend/openzaak/docker-compose.openzaak.yml(or an override file)- New script/config for
abonnementregistration docs/reference/openzaak-integration.md
Steps
- Add celery/celery-beat services to a notifications-enabled compose profile.
- Flip
NOTIFICATIONS_DISABLEDoff for that profile. - Script the
abonnementregistration (POST to the NRC, pointed at the BFF's/zgw/notificatiesendpoint from WP-52), idempotent on rerun. - Verify a real write (e.g. a status change) triggers a live webhook delivery to the BFF.
Acceptance criteria
- A notifications-enabled harness profile runs a celery worker and delivers a real notification end-to-end to the BFF's webhook.
- Provisioning is a script, re-runnable without erroring on an already-configured target.
What actually happened
The Decisions block assumed OpenZaak itself could be pointed at, celery-wired, and made to
deliver to a subscribed abonnement — checking the running image (grep -ril abonnement inside
the web container) found nothing: OpenZaak does not serve the Notificaties API. It's a
separate application (openzaak/open-notificaties, its own image/DB/celery/beat stack).
Standing one up for real abonnement/kanaal-filtered routing would mean ~5 new services (a
second Postgres, web, worker, beat, plus the NRC↔AC authorization chain) for a benefit this
harness doesn't need — there is exactly one subscriber (this repo's own BFF), never N. Re-scoped
before writing any code (confirmed with the user): OpenZaak's own NotificationsConfig points
straight at the BFF's webhook via a zgw_consumers.Service (auth_type=api_key) instead — no
NRC, no abonnement, same delivery proof (a real write → OpenZaak's celery worker → a real HTTP
POST → the BFF's audit trail). The two "no abonnement" acceptance-criteria words above were
edited out for the same reason.
docker-compose.openzaak.notificaties.yml— an opt-in overlay (notprofiles:, matching WP-55's prod-override precedent) adding one celery worker (not celery-beat:send_notificationis a plain async task fired on save, not a scheduled one — beat only matters on a real NRC's polling side) and flippingNOTIFICATIONS_DISABLEDoff. The two changes are inseparable:NOTIFICATIONS_GUARANTEE_DELIVERYdefaults true, so the moment that flag is false, every write to a notified resource 500s-and-rolls-back unlessNotificationsConfigalready has a client — hencebootstrap-notificaties.shconfiguring it is not a separate step.- Reaching the BFF from the worker turned out to be the real obstacle, not the Django/celery
wiring.
extra_hosts: host.docker.internal:host-gateway(the plan's first choice) resolves fine but every TCP connect through it timed out — confirmed live: this environment's rootless Podman doesn't route container→host-port traffic that way. Fix: join the overlay'sceleryservice to the repo root's owndocker compose upnetwork (external: true, by theatomic-design-poc_defaultname compose derives from the repo directory) and reach the BFF by its container name (api) instead — container-to-container, which this exact stack already proved reliable (celeryalready talks todb/redisthat way). One more trap on that path:docker compose run --name api ...does not register theapiDNS alias other containers need (onlydocker compose up -d apidoes) — cost a debugging round-trip before switching toup -d(via a temporary, uncommitteddocker-compose.override.yml) for the live verification. bootstrap-notificaties.sh—update_or_createon theService's fixed slug (idempotent); preflights the BFF's webhook with a synthetic notification body first (204 required) so a misconfigured target fails before touching OpenZaak, not after (a later write would otherwise 500-and-rollback with no obvious cause).verify-notificatie.sh— the runnable end-to-end check. First attempt triggered the write via a secondstatussenPOST (the "final" status) — 403'd: WP-57's narrowedzaken.aanmakenscope permits exactly one status per zaak ("Met de 'zaken.aanmaken' scope mag je slechts 1 status zetten"). Switched the trigger to a zaakPATCH(toelichting), covered by the already-grantedzaken.bijwerkenand trivially repeatable. Second attempt used the final statustype anyway for a different reason and got a 400 ("Zaak has no resultaat") — OpenZaak requires aresultaatbefore the closing status; thePATCHsidesteps that precondition entirely too.- Verified for real, twice:
bootstrap-catalogus.sh(idempotent re-run, all "exists") →bootstrap-notificaties.sh(preflight 204,Serviceconfigured) →verify-notificatie.sh(PATCH → polled/admin/audit→ found the deliveredzgw:notificatie/allowrow) → reran both WP-58 scripts again under the same running harness (still idempotent, delivered again). Also confirmed the negative case directly:POST /zgw/notificatieswith noAuthorizationheader, and with a wrong one, both 401 — the shared-secret gate isn't just accepting anything. Backend suite stayed green throughout (159/159,dotnet test --filter Category!=Integration). Test infrastructure (the temporarydocker-compose.override.yml, the manually-createdapicontainer) was torn down / reconciled back to the pre-session baseline afterward.
Verification
Bring up the notifications-enabled profile (backend/openzaak/README.md's "Notifications-enabled
profile" section); run ./bootstrap-catalogus.sh && ./bootstrap-notificaties.sh && ./verify-notificatie.sh. The last script fails loudly (with celery/worker log diagnostics) if no
delivered notification shows up in the BFF's /admin/audit within 60s.
Out of scope
Cache invalidation on notification receipt (flagged separately in
openzaak-integration.md as a ponytail: marker, not part of this slice);
celery-flower/monitoring UI. A real Notificaties API (NRC) + abonnement/kanaal-filtered
routing (see "What actually happened") — add one if a later WP needs more than this harness's
single subscriber.
Risks
Celery adds real operational surface (another process to keep alive) — scope this WP to
"works, documented," not a fully monitored deployment. The direct-to-BFF shortcut means this
harness doesn't exercise real abonnement/kanaal-filter validation — a production deployment's
NRC-based path (documented in openzaak-integration.md) is untested by this harness by
construction.
Depends on: WP-56 (provisioning mechanism this extends).