## What & why S-19b-2, closing out ADR-0028's stated direction: **the read projection is now derived from the `RegisterRecord` in Objecten, not from ZGW zaak events.** Until now the subscriber listened on `zaken` and *inferred* register state from case events — a `zaak/create` meant INGEDIEND, and any `status/create` was assumed to be the approval (it may not read OpenZaak, so it could not tell statustypen apart). The reference wasn't in the notification at all, so every projection made a second hop to the ACL. The register — a fact about a person — was being reconstructed by guessing at the lifecycle of the case that produced it. - The subscriber's abonnement moves to the `objecten` kanaal (S-19b-1 made it publish). - An Objecten notification carries **no record data**, only the object URL, so the record is read back through the ACL (`POST /register-records/read`) — §8.1 applies to Objecten exactly as ADR-0028 established. - The record carries `id`, `status` and `reference`, so the row *is* the record: `IsZaakCreated`, `IsZaakStatusSet`, `ZaakUrl`, `ZaakId` and `ToEntry`'s `Resource == "status"` inference are all gone, and so is the ACL enrichment hop. - **The ACL now writes an INGEDIEND record on submit.** Without it, re-sourcing would silently drop every submitted registration from the public register, since only approval wrote a record. - `processed_notifications` holds the projected row (`register_id`, `status`, `reference`) instead of the ZGW event, so a rebuild is a replay with no mapping rules and no upstream reads at all. **ADR-0030** records it. ADR-0028's open caveat — record written but not yet read, "the two must agree" — is closed: there is one source now. Closes #153 ## Definition of Done - [x] Linked Gitea issue (above). - [x] Failing tests committed before the implementation — two red/green pairs, ACL side (06c0444→566ef7d) and subscriber side (142ed45→8af09b2). - [x] Refactor commit follows (b496ac9). - [x] Conventional Commits referencing the issue (`refs #153`). - [x] CI green — all six jobs onb30fa66, `verify-stack` end to end including the e2e. - [x] `docker compose up` from a fresh clone reaches green health checks within 3 minutes (`verify-stack`'s bring-up step — see the wait-healthy fix below). - [x] Docs updated — ADR-0030 added, ADR-0028's consequence + caveat annotated, BACKLOG.md, e2e header comment. - [x] ADR added in `docs/architecture/`. - [x] Demo note in `docs/demo-script.md` — n/a: no user-visible change. The openbaar register shows the same two statuses for the same registrations; only where they come from changed. ## Notes for reviewers **The decision I'd most like a second opinion on** is the one the issue didn't settle: what happens to INGEDIEND. Objecten held only INGESCHREVEN records, so re-sourcing forced a choice between (a) the ACL also writing on submit, (b) a public register that lists only actual registrations, or (c) a hybrid keeping both kanalen. I took (a): visible behaviour is unchanged and the register holds the whole lifecycle. (b) is arguably the better *semantics* for a public register but narrows what the portal shows and reads against PRD §68 ("~50 register entries with diverse statuses"); (c) leaves the projection half-derived from ZGW, which is the coupling ADR-0028 set out to remove. All three are laid out in ADR-0030. **The dedup key is the projected row**, `objecten:object:{url}:{status}:{reference}` — not the object URL (the ACL upserts *one object per registration*, so submit and approval notify about the same URL and the approval would be swallowed as a duplicate) and not URL+actie (a retried approval is a second `update`). Redeliveries collapse, genuine state changes don't. §8.6. **The migration drops columns rather than renaming them.** EF scaffolded renames — `resource` → `register_id`, `zaak_id` → `status` — which would have carried ZGW values into columns meaning something else, and a rebuild would then have projected that garbage. It also empties both tables: a pre-slice row describes a zaak event the new projector can't reproject, and those registrations have no RegisterRecord in Objecten either, so they're not re-derivable from the new source. Stated as a ceiling in the ADR — fine while stacks are ephemeral, backfill from Objecten if a long-lived environment ever needs it. **`run-projection-check.sh` now opens its zaak through the ACL** instead of straight against OpenZaak, because the ACL is what writes the record. A zaak created behind the ACL's back produces no projection row — that's the re-source working, not a gap. ## Three fixes CI found, none of them in the projection logic 1. **`wait-healthy.sh` matched the wrong container** (744f91a). Bring-up timed out with `TIMEOUT: 'objecten' not healthy (status=none)` while the `docker ps` it dumps showed objecten `Up 9 minutes (healthy)`. `--filter name=` is a substring match, so `objecten` also matches `objecten-db`/`objecten-redis`/`objecten-celery`, and `head -1` took whichever docker listed first — the celery worker has no healthcheck, hence `status=none`. Latent since those services landed and decided purely by listing order; `objecttypen` matches `objecttypen-db` the same way. Anchored on the compose replica suffix, which the verify scripts already do. 2. **The ACL had to be repointed at OpenZaak's IP** (7e0897a). Opening the zaak through the ACL put this check in the same bind run-domain-check.sh already handles: `400 {"name":"zaaktype","code":"bad-url","reason":"Voer een geldige URL in."}`. OpenZaak reflects the request Host into the zaaktype URL and then rejects it on zaak-create when single-label — the mechanism compose already documents on `ACL_OPENZAAK_BASEURL`. 3. **Approval arrives as `partial_update`, not `update`** (0dd26a7→b30fa66) — the one real bug in the slice. The ACL upserts with PATCH; DRF routes it through the notifying `update()` but names the action `partial_update`, so the projector dropped every approval. Only the e2e could catch it: `verify-projection` drives a submit, and per ADR-0028 the e2e is the only check that drives a *real* approval. `verify-tracing` also failed once (run 722) on a path this PR doesn't touch, and passed on a plain re-run of the same commit. Tempo logged `pusher failed to consume trace data` / `distributor_pool failing healthcheck` — it dropped spans under runner load rather than the trace chain being broken. Filed as **#156** rather than absorbed here. **Correction to the #152 PR notes:** I wrote there that celery concurrency was "the next knob" if verify-stack got tight. It isn't — `CELERY_WORKER_CONCURRENCY` already defaults to 1 in the Maykin image, so `objecten-celery` is already a single-process worker. Noted in #156. **Possible follow-up, deliberately not done here:** an `openzaak.local` network alias mirroring `objecten.local` would remove the ACL-repoint dance from both run-domain-check.sh and run-projection-check.sh. It changes the host in every zaak URL the system produces, which is too broad a ripple to land inside an unrelated slice — worth its own issue. **Known costs, all in the ADR:** submission is now two writes across two modules and eventually consistent (same posture ADR-0028 accepted for approval); projecting now depends on the ACL being reachable on the main path, not just for enrichment (NRC retries, so it converges); and OpenZaak still publishes to `zaken` with nothing in the product listening — kept because `verify-nrc` asserts that path.Reviewed-on: #155
365 lines
22 KiB
Markdown
365 lines
22 KiB
Markdown
# BACKLOG.md — iteration plan (curated mirror of the active Gitea milestone)
|
||
|
||
> **Source of truth: Gitea Issues + Milestones.** This file is a human-readable mirror so contributors and Claude Code can see the iteration plan at a glance. When the two disagree, **Gitea wins** and this file is regenerated.
|
||
>
|
||
> Regenerate this file when a milestone is opened, closed, or significantly reshaped. A commit message of `docs(backlog): sync with milestone <name>` is appropriate.
|
||
|
||
Each slice has a unique ID (`S-NN`) matching the title of its Gitea issue. Work is pulled from the **active milestone**'s project board in Gitea, not from this file.
|
||
|
||
Each slice is **independently demoable** and meets the Definition of Done in `CLAUDE.md` §3. Pull only one slice at a time.
|
||
|
||
---
|
||
|
||
## Iteration 0 — Foundations *(milestone: `Iteration 0 — Foundations`)*
|
||
|
||
> **S-00 was split** (CLAUDE.md §13) into the sub-slices below. The original
|
||
> outcome — a fresh clone + `docker compose up` reaching a green BFF health
|
||
> endpoint, with CI green and the contributor scaffolding in place — is the sum
|
||
> of S-00-a…e. The Gitea milestones, labels, and slice issues already exist —
|
||
> they are managed directly with the `tea` CLI.
|
||
|
||
### S-00-a · Placeholder BFF + health endpoint
|
||
|
||
**Outcome:** A minimal .NET BFF exposing `GET /health` returning green; runnable with `dotnet run`. TDD anchor for the slice.
|
||
|
||
**Touches:** `services/bff/`, tests. **Out of scope:** Docker, CI, OIDC.
|
||
|
||
### S-00-b · Dockerfile + compose skeleton + compose-up smoke
|
||
|
||
**Outcome:** BFF containerized; `infra/docker-compose.yml` brings it up; health green within 3 minutes from a fresh clone.
|
||
|
||
**Touches:** `services/bff/Dockerfile`, `infra/docker-compose.yml`, smoke script. **Out of scope:** other services, CI.
|
||
|
||
### S-00-c · Gitea Actions CI (lint, build, unit, compose-up smoke)
|
||
|
||
**Outcome:** `.gitea/workflows/ci.yaml` green on PRs and `main`: lint, build, unit, compose-up smoke. Actions pinned by absolute URL (§8.7/§15).
|
||
|
||
**Touches:** `.gitea/workflows/ci.yaml`, `docs/runbooks/ci.md`. **Out of scope:** mutation, e2e, container push.
|
||
|
||
### S-00-d · Contributor workflow: issue/PR templates, git-cliff, CHANGELOG
|
||
|
||
**Outcome:** Issue templates (`slice`/`bug`/`adr-proposal`), PR template, `git-cliff` → empty `CHANGELOG.md`, `docs/gitea-workflow.md`.
|
||
|
||
**Touches:** `.gitea/ISSUE_TEMPLATE/`, `.gitea/PULL_REQUEST_TEMPLATE.md`, `cliff.toml`, `CHANGELOG.md`, `docs/gitea-workflow.md`. **Out of scope:** app code.
|
||
|
||
### S-00-e · Docs scaffold: MkDocs + ADR-0001 + README quickstart
|
||
|
||
**Outcome:** MkDocs builds the `docs/` site; `docs/architecture/adr-0001-loose-coupling.md` exists; README has a sub-10-minute quickstart.
|
||
|
||
**Touches:** `mkdocs.yml`, `docs/` nav, `docs/architecture/adr-0001-loose-coupling.md`, `README.md`. **Out of scope:** Gitea Pages publish.
|
||
|
||
---
|
||
|
||
## Iteration 1 — Walking skeleton *(milestone: `Iteration 1 — Walking Skeleton`)*
|
||
|
||
The skeleton proves the spine end-to-end: a registration, a workflow, a zaak in OpenZaak, an event back, a public projection. Minimum viable but real.
|
||
|
||
### S-01 · OpenZaak + Open Notificaties + Postgres come up in compose
|
||
|
||
**Outcome:** Local `docker compose up` brings up OpenZaak, Open Notificaties, their dependencies, and a seeded ZTC catalogus called `BIG`. A health check confirms all reachable.
|
||
|
||
**Acceptance:**
|
||
|
||
- `curl` to OpenZaak `/zaken/api/v1/` returns 401 (auth working).
|
||
- A test client with a generated JWT can list zaaktypen in the `BIG` catalogus.
|
||
- The seeded catalogus contains one lean `BIG-registratie` zaaktype with only the schema-mandatory fields plus `bsn` as an eigenschap.
|
||
|
||
**Touches:** `infra/openzaak/`, `infra/opennotificaties/`, `infra/seed/`, ADR for catalogus design.
|
||
|
||
**Out of scope:** any portal, BFF, Flowable, ACL code.
|
||
|
||
### S-02 · Keycloak with mock DigiD, eHerkenning, eIDAS, medewerker realms
|
||
|
||
**Outcome:** Keycloak runs locally with four realms pre-seeded. Each realm has 1–2 test users with known credentials documented in `docs/synthetic-data.md`.
|
||
|
||
**Acceptance:**
|
||
|
||
- Browser-based OIDC login flow works for each realm against a placeholder client.
|
||
- Mock DigiD realm returns a BSN claim; eHerkenning returns a KvK; eIDAS returns a foreign identifier; medewerker returns role claims.
|
||
|
||
**Touches:** `infra/keycloak/`, seed scripts.
|
||
|
||
**Out of scope:** real federation, MFA.
|
||
|
||
### S-03 · Flowable up with a minimal BPMN: "Registratie ontvangen"
|
||
|
||
**Outcome:** Flowable runs locally with Postgres. A single BPMN model (`registratie.bpmn`) deployed with one start event, one external task `OpenZaakAanmaken`, one end event.
|
||
|
||
**Acceptance:**
|
||
|
||
- BPMN model deployed via Flowable's REST API on container start.
|
||
- An HTTP call can start a process instance and observe it waiting on the external task.
|
||
|
||
**Touches:** `infra/flowable/`, `workflows/registratie.bpmn`.
|
||
|
||
**Out of scope:** DMN, boundary timers, second model.
|
||
|
||
### S-04 · ACL skeleton with one operation: open a zaak
|
||
|
||
**Outcome:** A .NET library + service that exposes one method: `OpenZaak(domainPayload) → zaakUrl`. It default-fills `bronorganisatie`, `verantwoordelijkeOrganisatie`, `startdatum`, `vertrouwelijkheidaanduiding`, and posts to OpenZaak. **Strict TDD throughout.**
|
||
|
||
**Acceptance:**
|
||
|
||
- BDD scenario: "Given a domain registration payload, when I call the ACL, then a zaak exists in OpenZaak with the default-filled fields."
|
||
- Mutation score baseline captured and enforced by the Gitea Actions pipeline.
|
||
- Integration test using Testcontainers against real OpenZaak passes.
|
||
|
||
**Touches:** `services/acl/`, tests, ADR for default-fill strategy.
|
||
|
||
**Out of scope:** all other ZGW operations, status transitions, documents.
|
||
|
||
### S-05 · BIG Domain Service skeleton with the Registration aggregate
|
||
|
||
**Outcome:** A .NET service exposing a single endpoint `POST /registrations`. The Registration aggregate has a state machine with at minimum `INGEDIEND`. The service orchestrates: start a Flowable process → external task callback executes the ACL `OpenZaak` → zaak URL stored on the aggregate.
|
||
|
||
**Acceptance:**
|
||
|
||
- BDD scenario: "Given a zorgprofessional submits a registration, when the domain service receives it, then a Flowable process is started and a zaak is opened in OpenZaak."
|
||
- Integration test exercises the full path (no real frontend yet).
|
||
- The Workflow Client is the only code that calls Flowable.
|
||
|
||
**Touches:** `services/domain/`, `services/acl/` (consumed), tests, ADR for external-task job-worker pattern.
|
||
|
||
**Out of scope:** any other use case, documents, decisions.
|
||
|
||
### S-06 · Event Subscriber + Read Projection (minimal)
|
||
|
||
**Outcome:** An NRC webhook consumer that, on `zaak.gecreeerd`, writes a row to a `register_projection` table with `id`, `bsn`, `naam_placeholder`, `status`. Idempotent. Rebuildable.
|
||
|
||
**Acceptance:**
|
||
|
||
- BDD scenario: "Given a zaak is created in OpenZaak, when the NRC event is delivered, then the projection contains a row with status INGEDIEND."
|
||
- Replaying the same event twice does not create duplicates.
|
||
- A `projection rebuild` admin command repopulates from OpenZaak.
|
||
|
||
**Touches:** `services/event-subscriber/`, `services/projection-api/`, tests.
|
||
|
||
**Out of scope:** decision events, multiple projections, public-safe field filtering (will tighten in S-09).
|
||
|
||
### S-07 · BFF with one endpoint per portal + OIDC validation
|
||
|
||
**Outcome:** A .NET BFF exposing four endpoint groups (one per portal). Validates tokens issued by Keycloak. Implements the minimum needed for the walking skeleton: `POST /self-service/registrations`, `GET /openbaar/register?q=...`.
|
||
|
||
**Acceptance:**
|
||
|
||
- BDD scenarios cover the two endpoints with valid and invalid tokens.
|
||
- OpenAPI spec generated and committed.
|
||
|
||
**Touches:** `services/bff/`, OpenAPI spec, tests.
|
||
|
||
**Out of scope:** behandelaar and beheer endpoints (later slices).
|
||
|
||
### S-08 · Self-Service portal (Angular, NL DS) — submit a registration
|
||
|
||
> **S-08 was split** (CLAUDE.md §13; issue #9 closed) into the sub-slices below — it bundled the
|
||
> Nx bootstrap, the generated client, the NL DS + DigiD form, and a full-stack Playwright e2e, well
|
||
> past 1–2 days. Each sub-slice is independently demoable and CI-green.
|
||
|
||
- **S-08a (#65)** · Nx monorepo + Angular tooling + CI Node lane. Placeholder `self-service` app; `nx lint/test/build` green in a new CI Node lane.
|
||
- **S-08b (#66)** · Generated api-client lib from `services/bff/openapi.json` (never hand-written, §10) + a mocked-BFF unit test.
|
||
- **S-08c (#67)** · Self-service submit form — NL Design System `libs/ui`, DigiD OIDC `libs/auth`, component tests (Angular Testing Library), axe WCAG 2.1 AA on the submit page.
|
||
- **S-08d (#68)** · Playwright happy-path e2e (login → submit → success) against the full stack + compose serving + CI e2e lane.
|
||
|
||
**Out of scope (whole of S-08):** document upload, status tracking page.
|
||
|
||
### S-09 · Openbaar Register portal — public lookup *(#10)*
|
||
|
||
**Outcome:** The openbaar Angular app shows a search box. Anonymous. Queries the BFF's `/openbaar/register` which reads only the projection's **public-safe** fields. Shows the public-visibility half of the walking skeleton.
|
||
|
||
_Split from the original S-09 — scoped to the portal only; the approval flow is **S-09b (#75)**._
|
||
|
||
**Acceptance:**
|
||
|
||
- E2E test: after a zorgprofessional registers via self-service (S-08), the openbaar register shows the entry (as `INGEDIEND`).
|
||
- Public-safe field whitelist enforced and tested (already in the BFF; add a portal component test + a11y check).
|
||
|
||
**Touches:** `apps/openbaar/`, compose serving, e2e, docs.
|
||
|
||
**Out of scope:** approval/status transition (S-09b), advanced search filters, sorting.
|
||
|
||
### S-09b · Approval flow — temp admin endpoint + status transition to projection *(#75)*
|
||
|
||
**Outcome:** A behandelaar approves a submitted registration via a temporary admin endpoint (no behandel-portal yet — S-12). The approval transitions the zaak status through the ACL → NRC → event-subscriber → projection, and the openbaar register then shows the entry as approved.
|
||
|
||
**Acceptance:**
|
||
|
||
- A new terminal/approved status (e.g. `INGESCHREVEN`) exists and is projected.
|
||
- Temporary admin approve endpoint transitions a registration via a real ZGW status set (behind the ACL, §8).
|
||
- E2E: register (S-08) → approve → openbaar shows the entry as approved.
|
||
|
||
**Touches:** `services/domain`, `services/acl`, `services/event-subscriber`, `services/projection-api`, e2e.
|
||
|
||
**Out of scope:** behandel-portal UI (S-12), assessment logic (S-13), escalation (S-15).
|
||
|
||
**End of walking skeleton** (S-09 + S-09b). Demo: submit → process → projection → public visibility. All CI gates green on Gitea Actions. Cut release `vYYYY.MM.0` and publish via Gitea Releases.
|
||
|
||
---
|
||
|
||
## Iteration 2 — Flow completeness and exception handling *(milestone: `Iteration 2 — Flow Completeness`)*
|
||
|
||
### S-10 · Document upload + boundary timer for document timeout (Flow 2)
|
||
|
||
Split (issue #11 closed) into two independently-demoable slices per §13 — the original spanned six net-new surfaces including a new ZGW boundary:
|
||
|
||
#### S-10a · Document-wait task + 30-day timeout cancellation + provision trigger — #102
|
||
|
||
**Outcome:** BPMN gains a `WachtOpDocumenten` user task with a 30-day (P30D) interrupting boundary timer. On timeout the case is cancelled — the timer runs to a dedicated cancel end-event and the domain aggregate moves to a new terminal status `Verlopen` via an external-worker (mirrors S-14 escalation / S-11 withdrawal). "Documents received" is wired end-to-end (domain endpoint + BFF + a "Documenten aanleveren" button on the self-service page) so the walking-skeleton e2e stays green — but the document is **not yet stored** in ZGW; that is S-10b.
|
||
|
||
**Acceptance:** BDD both branches (documents-in-time vs timeout-cancel); live timer-fire via the management-API "move" idiom; the registration e2e provides documents before the behandelaar step.
|
||
|
||
#### S-10b · Real diploma upload stored via the ACL Documenten API — #103
|
||
|
||
**Outcome:** the self-service "Documenten aanleveren" action becomes a real file upload; the file (base64-encoded end-to-end) is stored in the ZGW Documenten (DRC) API as an `enkelvoudiginformatieobject` and related to the zaak, with all document calls routed through the ACL (§8.1, ADR-0018). Builds on the S-10a trigger/wait. Depends on #102.
|
||
|
||
**Acceptance:** ACL Documenten gateway integration test (real OpenZaak); Playwright e2e uploads a real PDF.
|
||
|
||
#### S-10c · Close the ZGW zaak on document-timeout expiry — #106
|
||
|
||
**Outcome:** when the 30-day term lapses (S-10a `RegistratieVerlopen`), the ZGW zaak is set to a distinct non-terminal `Geannuleerd` status + `Vervallen` resultaat (not just the domain aggregate → `Verlopen`), resolved by name in the ACL. Adds the cancellation statustype/resultaattype to the seed + an ACL `CancelZaakAsync`/`POST /annuleringen` + expiry-worker wiring. Carved from S-10b (ADR-0017/0018/0019). Depends on #103.
|
||
|
||
**Acceptance:** ACL↔OpenZaak integration test (cancellation records `Geannuleerd` + a resultaat, live); the domain verify script fires the P30D timer and asserts the zaak reaches `Geannuleerd` end-to-end; BDD asserts the zaak is cancelled on timeout but untouched when documents arrive in time.
|
||
|
||
### S-11 · Withdrawal (Flow 3)
|
||
|
||
**Outcome:** Self-service portal has a "trek aanvraag in" action. Domain service issues a withdraw command; BPMN message event correlates; case cancels with audit trail.
|
||
|
||
### S-12 · Behandel-portal — werkbak + beoordeling
|
||
|
||
**Outcome:** Behandel portal with login (medewerker realm), werkbak listing INGEDIEND/IN_BEHANDELING cases, claim and complete user tasks via Flowable, decision endpoint via Domain Service.
|
||
|
||
**Acceptance:** BDD scenarios for claim, complete, request additional document, decide.
|
||
|
||
### S-13 · DMN decision: diploma eligibility (Flow 4)
|
||
|
||
**Outcome:** A DMN decision table evaluated by the Domain Service via Workflow Client. Foreign diplomas route to an extra "CBGV-advies" user task in BPMN.
|
||
|
||
**Acceptance:** BDD scenarios for domestic and foreign diploma paths; DMN evaluated separately is unit-tested.
|
||
|
||
### S-14 · Beoordeling escalation (Flow 5)
|
||
|
||
**Outcome:** Boundary timer on beoordeling user task — 14 days. On timeout, reassigns to a teamlead role.
|
||
|
||
### S-26 · Self-service — resume an existing registration after refresh — #111
|
||
|
||
**Outcome:** a signed-in zorgprofessional who reloads the self-service portal (or returns later) gets back to their in-flight registration and its actions (Documenten aanleveren, Trek aanvraag in), instead of a blank submit form with the reference lost. Today all post-submit state lives in in-memory signals, the reference is not in the URL, and there is no self-service read endpoint — so a reload strands the registration. Adds an owner-scoped (DigiD bsn) `GET /self-service/registrations` on the BFF/domain and a load-on-init/route restore in the portal.
|
||
|
||
**Acceptance:** BDD — resume after refresh shows the existing registration; lookup is owner-scoped (never another citizen's); a user with no in-flight registration still sees the submit form. Playwright e2e reloads mid-flow and asserts the actions remain reachable.
|
||
|
||
---
|
||
|
||
## Iteration 3 — Maintenance portal and observability *(milestone: `Iteration 3 — Beheer & Observability`)*
|
||
|
||
### S-15 · Beheer-portal — catalogus & default-fill rules *(split — #16 closed)*
|
||
|
||
**Outcome:** Beheer portal lets an admin view ZTC catalogi (read-only first), and manage the ACL's default-fill configuration via a CRUD UI. MFA on the medewerker realm enforced.
|
||
|
||
Split into independently deployable sub-slices (CLAUDE.md §13):
|
||
|
||
- **S-15a** (#130) · Beheer portal skeleton + read-only catalogi viewer — new beheer Angular app (medewerker-realm login) showing ZTC catalogi/zaaktypen read-only, via a BFF `/beheer/*` read endpoint proxying a read-only ACL Catalogi endpoint (§8.1, reuses the ADR-0021 Catalogi client).
|
||
- **S-15b** (#131) · ACL default-fill configuration CRUD — the `Acl__Defaults__*` config (ADR-0003) becomes a managed store with CRUD via the BFF + a portal UI. Depends on S-15a.
|
||
- **S-15c** (#132) · Enforce MFA (OTP) on the Keycloak medewerker realm.
|
||
|
||
### S-16 · OpenTelemetry traces + Grafana dashboard *(split — #17 closed)*
|
||
|
||
**Outcome:** Traces span portal → BFF → Domain → ACL → OpenZaak and portal → BFF → Domain → Flowable. Grafana dashboards pre-built for golden signals.
|
||
|
||
Split into independently deployable sub-slices (CLAUDE.md §13):
|
||
|
||
- **S-16a** (#122) · Observability backplane — Grafana Tempo + Prometheus + Grafana in compose, datasources auto-provisioned (ADR-0023). No collector; config baked into built images.
|
||
- **S-16b** (#123) · Distributed traces across the five .NET services (OTLP → Tempo; traceparent propagates via the typed HttpClients). Depends on S-16a. ✅
|
||
- **S-16c** (#124) · Prometheus metrics + golden-signal Grafana dashboards. Depends on S-16a. ✅
|
||
|
||
### S-17 · Quartz.NET scheduler — herregistratie reminder sweep ✅
|
||
|
||
**Outcome:** Daily Quartz.NET cron job finds inscriptions within 90 days of their herregistratie deadline and reminds each (flag on the aggregate + log). No outbound notification and no domain event in v1 — the reminder is the persisted flag, surfaced on the read model (ADR-0022, #120). Quartz fires time-triggered sweeps; the existing pumps stay as queue-drainers.
|
||
|
||
---
|
||
|
||
## Iteration 4 — Objecten and the authoritative register *(milestone: `Iteration 4 — Objecten`)*
|
||
|
||
### S-18 · Objecten + Objecttypen up in compose; Register objecttype defined *(split — #19 closed)*
|
||
|
||
**Outcome:** Objecten and Objecttypen running. A `RegisterRecord` objecttype defined with the public-safe schema.
|
||
|
||
Split into independently deployable sub-slices (CLAUDE.md §13):
|
||
|
||
- **S-18a** (#139, ✅) · Objecttypen API up in compose (own DB + seeded config + health + static token).
|
||
- **S-18b** (#140, ✅) · Objecten API up in compose, wired to Objecttypen. Depends on S-18a.
|
||
- **S-18c** (#141, ✅) · RegisterRecord objecttype defined + registered (public-safe JSON schema). Depends on S-18a/b.
|
||
|
||
### S-19 · ACL extension: write register-record to Objecten on approval *(split — #20 closed)*
|
||
|
||
**Outcome:** Approval path writes the canonical register record to Objecten, not OpenZaak eigenschappen. Projection now sourced from Objecten events.
|
||
|
||
**ADR required:** "Why Objecten holds the register, OpenZaak holds the process."
|
||
|
||
Split into independently deployable sub-slices (CLAUDE.md §13):
|
||
|
||
- **S-19a** (#149, ✅) · ACL writes the `RegisterRecord` to Objecten on approval, idempotently, alongside the ZGW eindstatus. Carries the ADR (ADR-0028).
|
||
- **S-19b** (#150, ✅) · Read projection sourced from Objecten instead of NRC zaak events. *(split — #150 closed)*
|
||
- **S-19b-1** (#152, ✅) · Objecten publishes to NRC — broker, celery worker, `objecten` kanaal, notifications config. Turns back on what ADR-0028 deliberately disabled.
|
||
- **S-19b-2** (#153, ✅) · Projection derived from `RegisterRecord` objects, rebuildable from the Objecten-derived log. The ACL also writes an INGEDIEND record on submit, so the register holds the whole lifecycle. Carries ADR-0030.
|
||
|
||
---
|
||
|
||
## Iteration 5 — Data governance module *(milestone: `Iteration 5 — Data Governance`)*
|
||
|
||
### S-20 · OpenMetadata module + seed bundle deployed alongside
|
||
|
||
**Outcome:** OpenMetadata stack runs as a separate compose file (`infra/governance/`). Seed bundle loaded: glossary, classification taxonomy, roles, default DQ tests.
|
||
|
||
### S-21 · Read-replica ingestion + API ingestion
|
||
|
||
**Outcome:** Postgres read replicas of domain, Flowable, projection. OpenMetadata ingestion connectors discover schemas. API connector ingests OpenZaak and Objecten via OpenAPI.
|
||
|
||
### S-22 · Lineage SDK (.NET) + lineage assertions across the personal-data path
|
||
|
||
**Outcome:** A thin .NET package wrapping OpenMetadata's lineage API, published to the **Gitea Packages** registry. ACL, Event Subscriber, and Domain Service call it as personal data flows. Each lineage edge carries purpose and legal basis.
|
||
|
||
**ADR required:** "Lineage as a property of code, not docs."
|
||
|
||
### S-23 · GDPR reporting cookbook
|
||
|
||
**Outcome:** `docs/gdpr-reporting.md` showing how to answer specific AVG questions using OpenMetadata (data subject request, processing register, lineage trace).
|
||
|
||
---
|
||
|
||
## Iteration 6 — Production aspirations (sketch only) *(milestone: `Iteration 6 — Production Posture`)*
|
||
|
||
### S-24 · Helm chart (sketch) + Kubernetes manifests for the platform
|
||
|
||
**Outcome:** A non-deployed-but-reviewable Helm chart and accompanying ADR on production posture. Documents HA, secrets, backup, observability, identity wiring.
|
||
|
||
### S-25 · Runbook completeness review
|
||
|
||
**Outcome:** All runbooks complete: startup, seed, common failures, upgrade upstream modules, restore from backup, rotate secrets, Gitea Actions gotchas.
|
||
|
||
---
|
||
|
||
## How to add a new slice
|
||
|
||
1. **Open a Gitea issue** using the `slice.md` template. Title format: `S-NN · One-line outcome`.
|
||
2. Assign it to the appropriate milestone and apply `type:slice` plus area labels.
|
||
3. Fill in: outcome (one sentence, user-visible if possible), acceptance (Gherkin or testable assertions), touches (services and folders), out of scope (explicit).
|
||
4. Add it to the milestone's project board in the `Todo` column.
|
||
5. Mirror it into this file under the relevant iteration heading.
|
||
6. Open a PR against `BACKLOG.md` for review **before** starting the work.
|
||
|
||
## How to split a slice
|
||
|
||
If a slice issue grows beyond ~1–2 days of focused work:
|
||
|
||
1. Identify the natural seam (often between backend and frontend, or between happy path and exception path).
|
||
2. **In Gitea,** close the original issue with a comment listing the replacement issues. Use `S-NN-a`, `S-NN-b`, etc., or just allocate fresh `S-NN` numbers.
|
||
3. Each split must still meet the Definition of Done independently.
|
||
4. Update `BACKLOG.md` in a `docs(backlog): split S-NN (refs #NN, closes #NN)` commit.
|
||
|
||
## Cross-references
|
||
|
||
- **System of record:** Gitea Issues (this repo's Issues tab).
|
||
- **Active board:** the Gitea project board on the current milestone.
|
||
- **Working agreements:** `CLAUDE.md`.
|
||
- **What we're building and why:** `PRD.md`.
|
||
- **Architecture diagrams + ADRs:** `docs/architecture/`.
|
||
- **Daily-changing operational notes:** Gitea Wiki.
|