S-19b-1. A write to the Objecten API now produces a delivered notification on the objecten kanaal in Open Notificaties. ADR-0028 switched Objecten's notifications off on
purpose — there was no broker, worker, kanaal or abonnement, so wiring only the client side
would have dropped every message on the floor. This slice builds the real path and turns it
back on.
objecten-celery worker (mirrors oz-celery) + CELERY_BROKER_URL/RESULT_BACKEND on
objecten-redis db 1 (db 0 is already the cache). notifications_api_common only queues
the send; without a worker every register write is silently undelivered.
nrc service + notifications_config in Objecten's setup_configuration, reusing the big-reference-seed credential OpenZaak publishes with (NRC authorizes it via OpenZaak's
AC, which grants it heeft_alle_autorisaties — no second credential needed).
The objecten kanaal in NRC's setup_configuration. The name is fixed by the Objects API
(NOTIFICATIONS_KANAAL), not chosen here; publishing to an unregistered kanaal is exactly
what the red check reported first.
NOTIFICATIONS_DISABLED: "false" in both compose files.
Writers address Objecten as objecten.local — see Notes for reviewers.
make verify-objecten-notifications — registers an abonnement on objecten pointing at a
throwaway sink, writes a RegisterRecord exactly as the ACL does on approval, asserts the
delivery. One assertion covering the whole chain: Objecten -> objecten-celery -> NRC ->
nrc-beat -> callback. Wired into the CI verify-stack job and the summary table.
ADR-0029 records the decisions; ADR-0028's ceiling now points at it.
Failing test committed before the implementation (dc9ca2c, red at the first hop: NRC POST /api/v1/abonnement -> 400 "Kanaal met deze naam bestaat niet.").
Implementation makes the test pass (4488962, + two fixes found by CI, below).
Conventional Commits referencing the issue (refs #152).
CI green — all six jobs on a5fd47e, including verify-stack end to end (e2e included).
docker compose up from a fresh clone reaches green health checks within 3 minutes
(verify-stack's bring-up step).
Demo note in docs/demo-script.md if user-visible — n/a, infrastructure only; nothing
consumes the kanaal until S-19b-2 (#153).
Notes for reviewers
The one genuinely non-obvious bit: writers address Objecten as objecten.local:8000, not objecten:8000. NRC types a notification's hoofdObject/resourceUrl as DRF URLField,
so Django's URLValidator runs on them — and it rejects a single-label host. Objecten
fills both from the object url DRF built with request.build_absolute_uri, i.e. the Host
the caller used. Writing via the plain service name returns 201 and then fails every
publish in the background, forever, with
400 {"hoofdObject":["Voer een geldige URL in."],"resourceUrl":["Voer een geldige URL in."]}
So the objecten service carries an objecten.local network alias and every writer uses it
— Acl__Objecten__BaseUrl, ObjectenGatewayIntegrationTests, this slice's verify driver.
An alias rather than a bare dotted SITE_DOMAIN so the host still resolves: a subscriber
following resourceUrl reaches the record, which S-19b-2 will do. Readers keep the plain
name. Same class of constraint as ADR-0028's Objecttypen base-URL rule.
Ceiling, stated in the ADR: nothing enforces the alias — a future writer using objecten:8000 gets a 201 and silently no notification. If a second writer ever appears,
rename the compose service rather than adding a lint.
Two CI-only failures on the way here, both worth knowing:
SITE_DOMAIN was my first guess at the mechanism and is simply not what builds those
URLs — dropped in d76abf2.
The check correlated the delivery on the reference inside the record it wrote. An NRC
notification carries kanaal/resource/kenmerken/hoofdObject/resourceUrl and never the record data, so it correlates on the object URL now (a5fd47e).
Cost: one more long-running container on the memory-tight runner. It inherits the capped UWSGI_PROCESSES: "1" env, which the celery command ignores; if verify-stack gets tight
again, celery concurrency is the next knob.
Follow-up: S-19b-2 (#153) sources the projection from these events. Nothing subscribes to
the objecten kanaal in the product yet — only the verify check does.
## What & why
S-19b-1. A write to the Objecten API now produces a **delivered** notification on the
`objecten` kanaal in Open Notificaties. ADR-0028 switched Objecten's notifications off on
purpose — there was no broker, worker, kanaal or abonnement, so wiring only the client side
would have dropped every message on the floor. This slice builds the real path and turns it
back on.
- `objecten-celery` worker (mirrors `oz-celery`) + `CELERY_BROKER_URL`/`RESULT_BACKEND` on
objecten-redis db 1 (db 0 is already the cache). `notifications_api_common` only *queues*
the send; without a worker every register write is silently undelivered.
- `nrc` service + `notifications_config` in Objecten's `setup_configuration`, reusing the
`big-reference-seed` credential OpenZaak publishes with (NRC authorizes it via OpenZaak's
AC, which grants it `heeft_alle_autorisaties` — no second credential needed).
- The `objecten` kanaal in NRC's `setup_configuration`. The name is fixed by the Objects API
(`NOTIFICATIONS_KANAAL`), not chosen here; publishing to an unregistered kanaal is exactly
what the red check reported first.
- `NOTIFICATIONS_DISABLED: "false"` in both compose files.
- Writers address Objecten as `objecten.local` — see *Notes for reviewers*.
- `make verify-objecten-notifications` — registers an abonnement on `objecten` pointing at a
throwaway sink, writes a `RegisterRecord` exactly as the ACL does on approval, asserts the
delivery. One assertion covering the whole chain: Objecten -> objecten-celery -> NRC ->
nrc-beat -> callback. Wired into the CI `verify-stack` job and the summary table.
**ADR-0029** records the decisions; ADR-0028's ceiling now points at it.
Closes #152
## Definition of Done
- [x] Linked Gitea issue (above).
- [x] Failing test committed before the implementation (dc9ca2c, red at the first hop:
`NRC POST /api/v1/abonnement -> 400 "Kanaal met deze naam bestaat niet."`).
- [x] Implementation makes the test pass (4488962, + two fixes found by CI, below).
- [x] Conventional Commits referencing the issue (`refs #152`).
- [x] CI green — all six jobs on a5fd47e, including `verify-stack` end to end (e2e included).
- [x] `docker compose up` from a fresh clone reaches green health checks within 3 minutes
(`verify-stack`'s bring-up step).
- [x] Docs updated — ADR-0029 added, ADR-0028's ceiling annotated, BACKLOG.md split.
- [x] ADR added in `docs/architecture/`.
- [x] Demo note in `docs/demo-script.md` if user-visible — n/a, infrastructure only; nothing
consumes the kanaal until S-19b-2 (#153).
## Notes for reviewers
**The one genuinely non-obvious bit: writers address Objecten as `objecten.local:8000`, not
`objecten:8000`.** NRC types a notification's `hoofdObject`/`resourceUrl` as DRF `URLField`,
so Django's `URLValidator` runs on them — and it rejects a **single-label** host. Objecten
fills both from the object url DRF built with `request.build_absolute_uri`, i.e. *the Host
the caller used*. Writing via the plain service name returns 201 and then fails every
publish in the background, forever, with
```
400 {"hoofdObject":["Voer een geldige URL in."],"resourceUrl":["Voer een geldige URL in."]}
```
So the `objecten` service carries an `objecten.local` network alias and every writer uses it
— `Acl__Objecten__BaseUrl`, `ObjectenGatewayIntegrationTests`, this slice's verify driver.
An alias rather than a bare dotted `SITE_DOMAIN` so the host still *resolves*: a subscriber
following `resourceUrl` reaches the record, which S-19b-2 will do. Readers keep the plain
name. Same class of constraint as ADR-0028's Objecttypen base-URL rule.
**Ceiling, stated in the ADR:** nothing enforces the alias — a future writer using
`objecten:8000` gets a 201 and silently no notification. If a second writer ever appears,
rename the compose service rather than adding a lint.
**Two CI-only failures on the way here**, both worth knowing:
1. `SITE_DOMAIN` was my first guess at the mechanism and is simply not what builds those
URLs — dropped in d76abf2.
2. The check correlated the delivery on the `reference` inside the record it wrote. An NRC
notification carries `kanaal`/`resource`/`kenmerken`/`hoofdObject`/`resourceUrl` and
**never the record data**, so it correlates on the object URL now (a5fd47e).
**Cost:** one more long-running container on the memory-tight runner. It inherits the capped
`UWSGI_PROCESSES: "1"` env, which the celery command ignores; if `verify-stack` gets tight
again, celery concurrency is the next knob.
**Follow-up:** S-19b-2 (#153) sources the projection from these events. Nothing subscribes to
the `objecten` kanaal in the product yet — only the verify check does.
not
added this to the Iteration 4 — Objecten milestone 2026-08-28 08:54:47 +00:00
Registers an abonnement on the `objecten` kanaal pointing at the existing
webhook sink, writes a RegisterRecord exactly as the ACL does on approval, and
waits for the delivery. That is the whole publish chain in one assertion:
Objecten → its celery worker → NRC → nrc-beat → the callback.
Fails today at the first hop, which is the point:
NRC POST /api/v1/abonnement → 400: {"name":"naam","code":"kanaal_naam",
"reason":"Kanaal met deze naam bestaat niet."}
Also splits S-19b (#150) into #152/#153 in BACKLOG.md — the notification wiring
and the projection re-source are independently deployable (CLAUDE.md §13).
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.
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.
The publish chain works — the sink received it:
{"kanaal": "objecten", "resource": "object", "kenmerken": {"objectType": "…"},
"hoofdObject": "http://objecten.local:8000/api/v2/objects/a68d4c46-…", …}
The check just looked for the wrong thing. An NRC notification carries hoofdObject /
resourceUrl and kenmerken — never the record data — so the `reference` inside the
RegisterRecord it wrote was never going to appear in the delivered message. Grep the sink
for the object URL instead, which is what identifies the write.
not
merged commit 2125fb0cfd into main2026-08-28 10:11:55 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What & why
S-19b-1. A write to the Objecten API now produces a delivered notification on the
objectenkanaal in Open Notificaties. ADR-0028 switched Objecten's notifications off onpurpose — there was no broker, worker, kanaal or abonnement, so wiring only the client side
would have dropped every message on the floor. This slice builds the real path and turns it
back on.
objecten-celeryworker (mirrorsoz-celery) +CELERY_BROKER_URL/RESULT_BACKENDonobjecten-redis db 1 (db 0 is already the cache).
notifications_api_commononly queuesthe send; without a worker every register write is silently undelivered.
nrcservice +notifications_configin Objecten'ssetup_configuration, reusing thebig-reference-seedcredential OpenZaak publishes with (NRC authorizes it via OpenZaak'sAC, which grants it
heeft_alle_autorisaties— no second credential needed).objectenkanaal in NRC'ssetup_configuration. The name is fixed by the Objects API(
NOTIFICATIONS_KANAAL), not chosen here; publishing to an unregistered kanaal is exactlywhat the red check reported first.
NOTIFICATIONS_DISABLED: "false"in both compose files.objecten.local— see Notes for reviewers.make verify-objecten-notifications— registers an abonnement onobjectenpointing at athrowaway sink, writes a
RegisterRecordexactly as the ACL does on approval, asserts thedelivery. One assertion covering the whole chain: Objecten -> objecten-celery -> NRC ->
nrc-beat -> callback. Wired into the CI
verify-stackjob and the summary table.ADR-0029 records the decisions; ADR-0028's ceiling now points at it.
Closes #152
Definition of Done
dc9ca2c, red at the first hop:NRC POST /api/v1/abonnement -> 400 "Kanaal met deze naam bestaat niet.").4488962, + two fixes found by CI, below).refs #152).a5fd47e, includingverify-stackend to end (e2e included).docker compose upfrom a fresh clone reaches green health checks within 3 minutes(
verify-stack's bring-up step).docs/architecture/.docs/demo-script.mdif user-visible — n/a, infrastructure only; nothingconsumes the kanaal until S-19b-2 (#153).
Notes for reviewers
The one genuinely non-obvious bit: writers address Objecten as
objecten.local:8000, notobjecten:8000. NRC types a notification'shoofdObject/resourceUrlas DRFURLField,so Django's
URLValidatorruns on them — and it rejects a single-label host. Objectenfills both from the object url DRF built with
request.build_absolute_uri, i.e. the Hostthe caller used. Writing via the plain service name returns 201 and then fails every
publish in the background, forever, with
So the
objectenservice carries anobjecten.localnetwork alias and every writer uses it—
Acl__Objecten__BaseUrl,ObjectenGatewayIntegrationTests, this slice's verify driver.An alias rather than a bare dotted
SITE_DOMAINso the host still resolves: a subscriberfollowing
resourceUrlreaches the record, which S-19b-2 will do. Readers keep the plainname. Same class of constraint as ADR-0028's Objecttypen base-URL rule.
Ceiling, stated in the ADR: nothing enforces the alias — a future writer using
objecten:8000gets a 201 and silently no notification. If a second writer ever appears,rename the compose service rather than adding a lint.
Two CI-only failures on the way here, both worth knowing:
SITE_DOMAINwas my first guess at the mechanism and is simply not what builds thoseURLs — dropped in
d76abf2.referenceinside the record it wrote. An NRCnotification carries
kanaal/resource/kenmerken/hoofdObject/resourceUrlandnever the record data, so it correlates on the object URL now (
a5fd47e).Cost: one more long-running container on the memory-tight runner. It inherits the capped
UWSGI_PROCESSES: "1"env, which the celery command ignores; ifverify-stackgets tightagain, celery concurrency is the next knob.
Follow-up: S-19b-2 (#153) sources the projection from these events. Nothing subscribes to
the
objectenkanaal in the product yet — only the verify check does.Registers an abonnement on the `objecten` kanaal pointing at the existing webhook sink, writes a RegisterRecord exactly as the ACL does on approval, and waits for the delivery. That is the whole publish chain in one assertion: Objecten → its celery worker → NRC → nrc-beat → the callback. Fails today at the first hop, which is the point: NRC POST /api/v1/abonnement → 400: {"name":"naam","code":"kanaal_naam", "reason":"Kanaal met deze naam bestaat niet."} Also splits S-19b (#150) into #152/#153 in BACKLOG.md — the notification wiring and the projection re-source are independently deployable (CLAUDE.md §13).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.The publish chain works — the sink received it: {"kanaal": "objecten", "resource": "object", "kenmerken": {"objectType": "…"}, "hoofdObject": "http://objecten.local:8000/api/v2/objects/a68d4c46-…", …} The check just looked for the wrong thing. An NRC notification carries hoofdObject / resourceUrl and kenmerken — never the record data — so the `reference` inside the RegisterRecord it wrote was never going to appear in the delivered message. Grep the sink for the object URL instead, which is what identifies the write.