# ADR-0012: One citizen-facing reference across self-service and the openbaar register - **Status:** Accepted - **Date:** 2026-07-14 - **Deciders:** Respellion engineering - **Relates to:** #78 (adr-proposal); builds on ADR-0008 (read projection), ADR-0001 (loose coupling), ADR-0009 (external-task worker / zaak creation) ## Context A citizen submits through the self-service portal and is shown a confirmation with a **reference** so they can find their registration back in the public register. But the two sides showed **different identifiers**: - The self-service confirmation shows the **domain `registrationId`** — a GUID minted by the domain aggregate (`RegistrationId.New()`) when the registration is created, before any zaak exists. - The openbaar register showed the **zaak id** — the UUID from the NRC `hoofdObject` URL, assigned by OpenZaak when the ACL opens the zaak. These never match, so the reference on the confirmation was useless for looking the entry up. The two identifiers live on opposite sides of the ACL boundary and are generated by different systems at different times, so there is no way to reconcile them after the fact without a correlating value carried across the boundary. The NRC notification the Event Subscriber consumes carries only the zaak URL plus the fixed `kenmerken` (`bronorganisatie`, `zaaktype`, `vertrouwelijkheidaanduiding`) — **not** the `registrationId`, the bsn, or the `identificatie`. ADR-0008 already recorded that filling any such field means reading the zaak **through the ACL** (§8.1) and deferred it as a follow-up. This is that follow-up, scoped to the one field the citizen actually needs. ## Decision **Use the domain `registrationId` as the zaak's `identificatie`, and surface that single value as the citizen-facing `reference` on both portals. The Event Subscriber enriches the projection with the reference by reading the zaak through the ACL, and stores it in the replay log so rebuild stays log-only.** Concretely, following the request path: 1. **Domain → ACL (write).** When the OpenZaak worker opens a zaak, it passes `registration.Id` to the ACL (`IAclClient.OpenZaakAsync(bsn, reference, …)`). The ACL sets it as the zaak's `identificatie` on `POST /zaken`. OpenZaak's `identificatie` is unique per `bronorganisatie` and ≤ 40 chars — a GUID string fits. The ACL remains the only code that constructs ZGW payloads (§8.1); the domain never sees a ZGW URL. 2. **Event Subscriber → ACL (read).** On a notification, the subscriber asks the ACL for the zaak's reference via a new `POST /zaken/reference` endpoint (`{ zaakUrl } → { reference }`), which reads the zaak's `identificatie` through the ACL's OpenZaak gateway. The subscriber still never talks to ZGW itself (§8.1) — it depends only on the ACL, over HTTP. 3. **Projection + replay log.** The reference is written both to the `register_projection` row **and** to the `processed_notifications` replay log (a new nullable `reference` column on each). Storing it in the log is what keeps ADR-0008's "**rebuild replays the log, not OpenZaak**" invariant true: `POST /admin/rebuild` reproduces the reference from the log without re-reading the ACL. 4. **BFF + openbaar.** The public view (`OpenbaarProjection.PublicView`) exposes `id`, `status`, and `reference` (never bsn/naam), and the openbaar search matches on either `id` or `reference`. The openbaar register's "Referentie" column now renders `reference`. The end-to-end guarantee is asserted in the Playwright walking-skeleton: the reference captured from the submit confirmation must appear as a cell in the public register. ### Why HTTP to the ACL, not the ACL as a library ADR-0008 floated "extend the ACL with a zaak-read operation, consumed as a library." We instead call the ACL **over HTTP**, consistent with every other cross-service hop in this system (portals→BFF, domain→ACL). Sharing the ACL as a library would couple the subscriber to the ACL's infrastructure assembly and its ZGW client configuration, defeating the anti-corruption boundary. The HTTP endpoint keeps the ACL the single owner of ZGW access and its config. ## Consequences **Positive** - One reference, end to end: the citizen's confirmation value is exactly what the public register shows and searches by. - §8.1 stays intact — only the ACL reads or writes ZGW; the subscriber depends on the ACL, not OpenZaak. - Rebuild stays log-only (ADR-0008): the reference is replayed from `processed_notifications`, so `/admin/rebuild` needs no ACL/ZGW access. - The column additions are nullable and additive; older rows without a reference are tolerated. **Negative / costs** - A new coupling: the Event Subscriber now depends on the ACL being reachable (`Acl__BaseUrl`, compose `depends_on: acl`). A registration whose reference read fails will need the notification redelivered (NRC already redelivers; the projection upsert is idempotent). - One extra HTTP hop per notification (subscriber→ACL→OpenZaak) on the projection path. Bounded: one small GET per zaak, off the citizen's request path. - `identificatie` now carries semantic meaning (it equals the `registrationId`). If OpenZaak were ever configured to auto-generate `identificatie`, the correlation would break; the ACL setting it explicitly is now load-bearing. ## Alternatives considered - **Carry the `registrationId` in the notification** — rejected: NRC `kenmerken` are fixed and the notification content is not ours to extend; it would also couple the projection to a bespoke notification shape. - **Show the zaak id on the confirmation instead** — rejected: the zaak does not exist yet when the confirmation is returned (the worker opens it asynchronously, ADR-0009), so the domain has no zaak id to show at submit time. - **Store only on the projection row, re-read the ACL on rebuild** — rejected: it would make rebuild depend on the ACL/ZGW, breaking ADR-0008's log-only rebuild invariant. - **Reconcile the two ids in a lookup table** — rejected: adds write-only state and a second source of truth for a value that can simply be the same on both sides.