Files
register-referentie/docs/architecture/adr-0029-objecten-publishes-to-nrc.md
T
not d76abf2df2
CI / build (pull_request) Successful in 1m9s
CI / lint (pull_request) Successful in 1m24s
CI / unit (pull_request) Successful in 1m32s
CI / frontend (pull_request) Successful in 3m13s
CI / mutation (pull_request) Successful in 6m25s
CI / verify-stack (pull_request) Failing after 8m11s
fix(infra): address Objecten by a dotted host so NRC accepts its notifications (refs #152)
The worker published and NRC answered 400 on every message:

  {"hoofdObject":["Voer een geldige URL in."],"resourceUrl":["Voer een geldige URL in."]}

NRC types both as DRF `URLField`, and Django's URLValidator refuses a single-label host.
Objecten fills them from the object `url` DRF built with `request.build_absolute_uri` —
the Host the *caller* used — so `SITE_DOMAIN` never entered into it. Dropped that env pair;
it was a wrong guess at the mechanism.

The fix is on the caller side: keep the `objecten.local` network alias and point every
writer whose writes must be notified at it — the ACL, the gateway integration tests, and
this slice's verify driver. Readers keep the plain service name.

ADR-0029 updated with the real mechanism and the ceiling it leaves: a new writer using
`objecten:8000` gets a 201 and silently no notification.
2026-08-28 11:31:31 +02:00

6.4 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): a zgw_consumers service nrc (api_type nrc) plus a notifications_config step naming it, and NOTIFICATIONS_DISABLED: "false" in both compose files.
  • NRC (infra/opennotificaties/setup_configuration/data.yaml): an objecten kanaal alongside zaken.
  • objecten-celery: a worker container on the Objecten image (/celery_worker.sh), mirroring oz-celery, with CELERY_BROKER_URL/CELERY_RESULT_BACKEND on objecten-redis db 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.

Writers address Objecten as objecten.local — NRC rejects single-label hosts

NRC types a notification's hoofdObject and resourceUrl as DRF URLFields, so Django's URLValidator runs on them — and it refuses a single-label host. Objecten fills both from the object url that DRF built with request.build_absolute_uri, i.e. the Host the caller used. Write to http://objecten:8000 and NRC answers every publish with

{"hoofdObject":["Voer een geldige URL in."],"resourceUrl":["Voer een geldige URL in."]}

which objecten-celery then retries with exponential backoff, forever, in the background — the write itself having returned 201.

SITE_DOMAIN does not fix this; it is not what builds those URLs. The fix is on the caller side: the objecten service carries an objecten.local network alias, and every component whose writes must be notified — the ACL (Acl__Objecten__BaseUrl), the gateway integration tests, this slice's verify check — addresses it there. An alias rather than a plain dotted SITE_DOMAIN so the host still resolves in-network: a subscriber that follows resourceUrl reaches the record it points at, which S-19b-2 will do. Readers are unaffected and keep using the plain service name.

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 another module then validates or dereferences.

  • ponytail ceiling: nothing enforces that a new writer uses the alias — it would get a 201 and silently no notification.
  • Upgrade path: if a second writer ever appears, rename the compose service to objecten.local so the plain name stops working, rather than adding a lint.

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-seed credential — a credential rotation now touches two modules.
  • Objecten now has two in-network names, and which one a caller uses silently decides whether its writes are notified (ceiling above).
  • 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.