Builds the four pieces ADR-0028 deliberately left absent, and turns
`NOTIFICATIONS_DISABLED` back off:
- `objecten-celery`, a worker on the Objecten image (mirrors `oz-celery`), plus
`CELERY_BROKER_URL`/`RESULT_BACKEND` on objecten-redis db 1 (db 0 is the cache).
Without it `notifications_api_common` queues the send and nothing ever ships it.
- An `nrc` service + `notifications_config` in Objecten's setup_configuration,
reusing the `big-reference-seed` credential OpenZaak publishes with.
- The `objecten` kanaal in NRC's setup_configuration — the name is fixed by the
Objects API (`NOTIFICATIONS_KANAAL`), and publishing to an unregistered kanaal is
what the failing check reported first.
- `SITE_DOMAIN: objecten.local:8000` + an `objecten.local` network alias: NRC
validates `hoofdObject`/`resourceUrl` with Django's URLValidator, which rejects a
single-label host, so `objecten:8000` is refused with "Voer een geldige URL in."
The alias keeps the dotted host resolvable so the URL still dereferences.
ADR-0029 records it; ADR-0028's ceiling now points there.
Makes `make verify-objecten-notifications` (dc9ca2c) pass.
5.6 KiB
ADR-0029: Objecten publishes register events to NRC
- Status: Accepted
- Date: 2026-08-14
- Deciders: Respellion engineering
- Slice: S-19b-1 (#152), first of the S-19b (#150) split
- Supersedes in part: ADR-0028's "Objecten's notifications are off for this slice"
Context
ADR-0028 put the authoritative register record in the Objecten API and had the ACL write
it on approval. It also switched Objecten's notifications off — deliberately, with a
stated ceiling: there was no broker, no worker, no objecten kanaal and no abonnement, so
turning the client side on alone would have produced a delivery path that looks wired and
drops every message.
S-19b-2 (#153) wants the read projection sourced from register writes rather than re-derived from ZGW zaak events. That needs the notifications to actually arrive. This ADR builds the four missing pieces and lifts the ceiling.
Decision
Objecten publishes to the same NRC OpenZaak already publishes to, on the objecten
kanaal, delivered by its own Celery worker — provisioned declaratively on both sides,
exactly as ADR-0007 did for OpenZaak.
- Objecten (
infra/objecten/setup_configuration/data.yaml): azgw_consumersservicenrc(api_typenrc) plus anotifications_configstep naming it, andNOTIFICATIONS_DISABLED: "false"in both compose files. - NRC (
infra/opennotificaties/setup_configuration/data.yaml): anobjectenkanaal alongsidezaken. objecten-celery: a worker container on the Objecten image (/celery_worker.sh), mirroringoz-celery, withCELERY_BROKER_URL/CELERY_RESULT_BACKENDonobjecten-redisdb 1 (db 0 is already the cache).
One NRC, one credential, one kanaal per publisher
Objecten reuses the big-reference-seed client OpenZaak publishes with. NRC verifies its
JWT and authorizes it against OpenZaak's Autorisaties API (ADR-0007), which grants that
client heeft_alle_autorisaties — so no second credential and no publisher-specific
authorization is needed. A second NRC, or a second credential, would buy isolation this
reference application has no use for.
The kanaal name is not ours to choose: the Objects API sends
NOTIFICATIONS_KANAAL = "objecten". NRC rejects a publish to an unregistered kanaal
("Kanaal met deze naam bestaat niet"), which is precisely what the failing check for this
slice reported first. Its filter set (object_type) matches the kenmerken the Objects API
sends, so an abonnement can narrow to one objecttype instead of receiving every write.
SITE_DOMAIN: objecten.local:8000 — NRC rejects single-label hosts
NRC validates a notification's hoofdObject/resourceUrl with Django's URLValidator,
which refuses a single-label host. Objecten builds those URLs from objects.utils.get_domain
(i.e. SITE_DOMAIN), so with the compose service name they read http://objecten:8000/...
and every publish is refused with "Voer een geldige URL in."
SITE_DOMAIN is therefore set to a dotted host, and the objecten service carries an
objecten.local network alias so that host still resolves in-network — a subscriber
that follows resourceUrl reaches the record it points at (which S-19b-2 will do). A
dotted name that didn't resolve would trade one broken link for a quieter one.
This is the same class of constraint as ADR-0028's "the ACL's Objecttypen base URL must
match Objecten's configured api_root": these modules put request-derived hosts into data
that another module then validates or dereferences.
A worker, not a synchronous send
notifications_api_common only schedules the send on transaction commit. Without a worker
the task sits in redis forever and every register write is silently undelivered — the exact
half-wired state ADR-0028 refused to ship. No beat for Objecten: it is a publisher, not a
subscriber, and nrc-beat already drains NRC's delivery queue.
Verification
make verify-objecten-notifications (infra/run-objecten-notifications-check.sh, in the
CI verify-stack job) registers an abonnement on the objecten kanaal pointing at a
throwaway webhook sink, writes a RegisterRecord exactly as the ACL does on approval, and
asserts the notification reaches the sink. That is the whole chain in one assertion:
Objecten → objecten-celery → NRC → nrc-beat → the callback. Any missing piece — broker,
worker, kanaal, notifications config — shows up as a non-delivery rather than as a green
config.
Consequences
Positive
- A register write is now observable by anything that subscribes, which is what S-19b-2 (#153) needs to make the projection a cache of Objecten rather than a re-derivation of ZGW.
- ADR-0028's ceiling is lifted: the delivery path is proven end to end, not merely configured.
Negative / costs
- One more long-running container (
objecten-celery) on an already memory-tight CI runner. - A second publisher on the shared
big-reference-seedcredential — a credential rotation now touches two modules. - One more hand-kept host constant:
SITE_DOMAINand theobjecten.localalias must stay in step, in both compose files. - ponytail ceiling: notification delivery has no dead-letter or alerting — a failed publish is visible only in the worker log.
- Upgrade path: if undelivered register events start mattering, subscribe an audit sink or read NRC's own delivery admin rather than building a retry layer here.
Coupling rules touched (CLAUDE.md §8)
None bent. This is infrastructure between two upstream modules, over their documented APIs; no service reaches another's database. §8.6 (idempotency at every event boundary) applies to whatever consumes the new kanaal — S-19b-2's problem, not this slice's.