Files
register-referentie/BACKLOG.md
Niek Otten bc9831c113
All checks were successful
CI / lint (push) Successful in 1m9s
CI / build (push) Successful in 53s
CI / unit (push) Successful in 1m4s
CI / frontend (push) Successful in 1m57s
CI / mutation (push) Successful in 5m19s
CI / verify-stack (push) Successful in 6m31s
S-09: Openbaar Register portal — public lookup (#76)
Anonymous openbaar portal completing the walking skeleton (submit → projection → public visibility).

closes #10
2026-07-13 14:35:34 +00:00

16 KiB
Raw Permalink Blame History

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 12 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 12 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)

Outcome: BPMN extended with a "wacht op documenten" user task with a 30-day boundary timer. Self-service portal supports diploma upload. On timeout the case is cancelled.

Acceptance: BDD scenarios for both branches; integration tests for the timer firing.

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.


Iteration 3 — Maintenance portal and observability (milestone: Iteration 3 — Beheer & Observability)

S-15 · Beheer-portal — catalogus & default-fill rules

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.

S-16 · OpenTelemetry traces + Grafana dashboard

Outcome: Traces span portal → BFF → Domain → ACL → OpenZaak and portal → BFF → Domain → Flowable. Grafana dashboards pre-built for golden signals.

S-17 · Quartz.NET scheduler — herregistratie reminder sweep

Outcome: Nightly job that finds entries within 90 days of expiry and emits a domain event. (No outbound notification in v1 — logged.)


Iteration 4 — Objecten and the authoritative register (milestone: Iteration 4 — Objecten)

S-18 · Objecten + Objecttypen up in compose; Register objecttype defined

Outcome: Objecten and Objecttypen running. A RegisterRecord objecttype defined with the public-safe schema.

S-19 · ACL extension: write register-record to Objecten on approval

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."


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 ~12 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.