## 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 ona5fd47e, 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 ind76abf2. 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.Reviewed-on: #154
register-reference
A reference application demonstrating how to build a Dutch government register (a BIG-style professional register) on top of unforked Common Ground modules — OpenZaak, Open Notificaties, Objecten, Open Klant — with loose coupling, modern workflow tooling, and data-governance ready integration points.
This repository is the runnable companion to Respellion's Foundations playbook entry on Common Ground architecture for non-municipal contexts (CIBG, DUO, RVO, and similar uitvoeringsorganisaties).
Status: under active development. See BACKLOG.md for the current iteration.
Start here
| Document | Purpose |
|---|---|
| docs/PRD.md | What we're building and why. Goals, non-goals, architecture summary, scope. Read once at project start. |
| CLAUDE.md | How we work. Engineering principles, TDD/DDD/BDD discipline, non-negotiable architectural rules, Gitea conventions. Read every task. |
| BACKLOG.md | Iteration plan. A curated mirror of the active Gitea milestone — Gitea Issues are the system of record. |
| docs/architecture/ | Diagrams (Mermaid sources) and ADRs. Start with adr-0001-loose-coupling.md. |
| docs/runbooks/ | Operational guides: local startup, seeding, common failures, CI debugging. |
The day-to-day operational pages — environment URLs, known issues right now, on-call notes — live in the Gitea Wiki for this repository. The wiki points at docs/ for anything authoritative.
What this application demonstrates
- A BIG-style professional register modelled on Dutch public-sector patterns, with four end-user portals: self-service, openbaar register, behandel-portal, beheer-portal.
- Common Ground modules as upstream peers — never forked, reached only via documented APIs (ZGW, NRC events).
- An Anti-Corruption Layer that confines all ZGW knowledge to one place, so the rest of the codebase stays domain-shaped rather than municipality-shaped.
- BPMN + DMN workflows via Flowable as a separate, swappable module — using the external-task job-worker pattern so BPMN models never reach into OpenZaak.
- A read projection as the public-facing data path, decoupled from the authoritative modules.
- Synthetic data and mock identity (Keycloak realms standing in for DigiD, eHerkenning, eIDAS, and a medewerker IdP) so the whole system runs locally without external dependencies.
- TDD, DDD, BDD, mutation testing, ADRs, and Conventional Commits as enforced defaults — encoded in CI.
- Gitea-native delivery: source, issues, milestones, project boards, releases, container registry, packages, wiki, and Actions.
For the architecture rationale, see docs/PRD.md §3 and docs/architecture/.
Local quickstart
Prerequisites
- .NET 10 SDK (for
make lint/build/unit) - A container engine with Compose v2 — Docker, or rootless Podman (see docs/runbooks/ci.md for the Podman + Compose-provider setup)
make,curl,git- ~4 GB free RAM, ~5 GB free disk (grows as services land)
Clone
git clone git@git.labs.respellion.tech:eho/register-referentie.git
cd register-referentie
Wired today (Iteration 0): only the placeholder BFF exists so far. Get to green in under 10 minutes — run the full check gate, or just the running service:
make ci # lint + build + unit + container smoke — the CI gate
docker compose -f infra/docker-compose.yml up -d --build --wait
curl http://localhost:8080/health # -> Healthy
--wait exits non-zero unless the container reports healthy, so it doubles as the compose-up smoke test. The remaining services and the URLs below land in later slices.
Target service URLs (most land in later slices)
| Service | URL |
|---|---|
| Self-Service portal | http://localhost:4200 |
| Openbaar register | http://localhost:4201 |
| Behandel-portal | http://localhost:4202 |
| Beheer-portal | http://localhost:4203 |
| BFF | http://localhost:8080 |
| OpenZaak | http://localhost:8000 |
| Open Notificaties | http://localhost:8001 |
| Flowable | http://localhost:8080 |
| Keycloak | http://localhost:8180 |
| MkDocs site (after build) | http://localhost:8000/docs/ |
Test credentials, BSNs, and personas: see docs/synthetic-data.md.
Build the docs site
python3 -m venv .venv && .venv/bin/pip install mkdocs-material
.venv/bin/mkdocs serve # live preview at http://localhost:8000
.venv/bin/mkdocs build # static site in ./site
Repository layout
register-reference/
├── apps/ # Angular portals (Nx monorepo)
│ ├── self-service/
│ ├── openbaar/
│ ├── behandel/
│ └── beheer/
├── libs/ # shared Angular libs (UI, auth, generated API client)
├── services/ # .NET services
│ ├── bff/
│ ├── domain/ # BIG Domain Service
│ ├── acl/ # Anti-Corruption Layer (the only code that knows ZGW)
│ ├── event-subscriber/
│ └── projection-api/
├── workflows/ # BPMN + DMN sources
├── infra/ # docker-compose, Keycloak, OpenZaak, Flowable, seed
├── tests/
│ ├── acceptance/ # Gherkin / Reqnroll BDD scenarios
│ └── e2e/ # Playwright
├── docs/ # versioned documentation (MkDocs source)
├── .gitea/ # Gitea Actions workflows, issue/PR templates
├── CLAUDE.md # working agreements
├── BACKLOG.md # iteration plan (mirror of active milestone)
└── README.md
Full description in docs/PRD.md §9.
Working in this repository
Source of truth for work: Gitea Issues + Milestones for this repository. BACKLOG.md is a mirror.
Branching: trunk-based. Short-lived branches off main, named <type>/<issue-number>-<short-slug> (e.g. feat/14-acl-default-fill).
Commits: Conventional Commits, referencing the Gitea issue:
feat(acl): default-fill bronorganisatie (refs #14)
The merging PR closes the issue via closes #14 in the squash-commit body.
Pull Requests: the unit of review. Squash-merged. PR template enforces the Definition of Done checklist from CLAUDE.md §3.
Releases: CalVer (YYYY.MM.PATCH), tagged on main, changelog generated by git-cliff, published as a Gitea Release with container images in the Gitea Container Registry.
See CLAUDE.md for the full working agreements, the architectural non-negotiables, and the rules Claude Code follows on every task.
Testing
- Unit tests — dominant. .NET (xUnit) and Angular (Vitest / Testing Library).
- Integration tests — Testcontainers-driven, exercising real OpenZaak, Flowable, NRC.
- Acceptance tests — Gherkin scenarios in
tests/acceptance/, one per user-visible flow. - End-to-end — Playwright, expanding slice by slice from the walking-skeleton happy path.
- Mutation testing — Stryker.NET and Stryker, baseline-ratcheted on
main.
Run everything:
./tools/test-all.sh
Run a focused slice (example):
dotnet test services/acl
Contributing
- Find or open a Gitea issue using one of the templates in
.gitea/ISSUE_TEMPLATE/(slice.md,bug.md,adr-proposal.md). - Assign yourself, move it to "In progress" on the milestone's project board.
- Branch off
main, follow the CLAUDE.md working agreements (TDD: red commit → green commit → refactor commit). - Open a PR using the template, link the issue, ensure the Gitea Actions pipeline is green.
- Squash-merge once approved. The merging commit closes the issue.
If a task pushes against any of the architectural rules in CLAUDE.md §8, stop and open an adr-proposal issue first. That conversation is more important than the code.
License and attribution
Respellion-authored code is licensed under EUPL-1.2. Upstream Common Ground modules retain their own licences (typically EUPL-1.2 or MIT — see each module's repository).
This reference application is not an official product of CIBG, DUO, VNG Realisatie, or any government body. It is a Respellion playbook artefact illustrating an architectural pattern.
Contact
- Issues, questions, proposals: open a Gitea issue on this repository.
- Architectural discussion: start with an
adr-proposalissue. - Anything sensitive: contact Respellion through the channel in
docs/runbooks/contact.md.