RD-03 moved the dashboard page to overzicht/ui/overzicht.page.ts and left four sections in registratie/ui/dashboard/. The folder was named after a page that lives in another context. A reader who opened it found four sections that are not the dashboard. The folder is now overzicht-secties/ — registratie's sections for the overzicht page. The alias does not change, because the sections stay in the registratie context. The story titles do not change, because they name the context. Five documents cited registratie/ui/dashboard.page.ts, a file that RD-03 renamed. They now name overzicht.page.ts, or the section that owns the behaviour they describe. The /dashboard route keeps its path. It is a user-visible URL. npm run ci --full passes: 67 and 45 storybook suites, 306 axe tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BIG-register Portals — Atomic Design POC
A two-app Angular monorepo showing how atomic design plus domain-driven boundaries make a frontend cheap to build, reuse and extend. The domain is the BIG-register (the Dutch register of healthcare professionals, run by CIBG): a citizen self-service portal and a case-handler backoffice, sharing one design system and one backend.
It is styled with the CIBG Huisstijl (a customized Bootstrap 5.2 build, vendored — ADR-0003) and built around one idea: make illegal states unrepresentable — in the UI's async states, in the domain types, and in the tests.
Demo / POC — no real login (DigiD is faked) and synthetic seed data. But the business rules and data are served by a real ASP.NET Core backend (
backend/) through a generated typed client, so the BFF + DDD design is demonstrable rather than hand-waved. A system-font stack stands in for the licensed Rijksoverheid font, and a text wordmark for the logo.
New here? Run
npm run storybookand open Foundations → Learning Path — a paced, hands-on three-day route through the codebase, written for a strong programmer who is new to frontend functional programming. For everything else,docs/README.mdis the documentation index.
Quick start
Everything at once — API, both portals:
docker compose up
# self-service portal → http://localhost:4200
# behandelportal → http://localhost:4201
# API + Swagger → http://localhost:5000/swagger
This does not start OpenZaak. The BFF uses a local, in-memory case store instead. To run against a real OpenZaak, see backend/openzaak/README.md.
Or run the pieces yourself:
npm install
npm start # self-service portal → :4200 (proxies /api → backend)
npm run start:behandelportal # case-handler portal → :4201
# in another terminal:
cd backend && dotnet run --project src/BigRegister.Api # API → :5000/swagger
npm run storybook # the design system + Foundations curriculum
npm run e2e # Playwright — starts the backend and app itself
Self-service flow: Login → Dashboard → Mijn gegevens → Registreren → Herregistratie → Intake
→ Brief. Behandelportal: Login → Werkvoorraad → Beoordeling (approve / reject / ask for more).
Admin pages (/beheer/*, /brief/huisstijl) need the admin role — see
roles and access.
Where to find things
| I want to… | Go to |
|---|---|
| learn the codebase from scratch | npm run storybook → Foundations → Learning Path |
| find any document | docs/README.md — the full index |
| understand the architecture | ARCHITECTURE.md |
| know why a decision was made | the ADRs — BFF-lite, contexts, huisstijl, stamdata, ZGW, test data |
| work on the backend / BFF | backend/README.md |
| run OpenZaak locally | backend/openzaak/README.md |
| see what shipped, or pick up work | docs/project/archive/backlog/README.md |
| build a feature the house way | .claude/skills/ — invocable recipes (new-feature, form-machine, …) |
| know the import rules | dependencies.md — enforced by dep:check |
| work on this repo as an AI agent | CLAUDE.md |
Repo map
| Path | What lives there |
|---|---|
apps/ssp/ |
Zorgverlener self-service portal (:4200) |
apps/behandelportal/ |
Behandelaar backoffice (:4201) |
libs/shared/ |
Design system, kernel, generated API client, Storybook Foundations docs |
libs/beheer/ |
Admin/stamdata context, used identically by both apps |
backend/ |
ASP.NET Core BFF (.NET 10, EF Core/SQLite), OpenZaak seam |
e2e/ |
Playwright specs |
docs/ |
reference/ (how + why) and project/ (backlog, PRDs) |
public/ |
Vendored CIBG Huisstijl + assets |
scripts/ |
CI gate, drift checks, generators |
Commands
npm run ci # ← run this before pushing: the whole gate, exactly what CI runs
| Task | Command |
|---|---|
| Run | npm start, npm run start:behandelportal, docker compose up |
| Test | npm test, npm run e2e, npm run test-storybook (axe on every story) |
| Check | npm run lint, typecheck, dep:check, check:tokens, check:seam |
| Build | npm run build, npm run build-storybook |
| Generate | npm run gen:api (typed client), npm run gen (plop: value object, form machine, context) |
| Docs | npm run storybook, npm run dep:graph, npm run gen:behaviour-spec |
npm run ci chains lint, typecheck, dependency boundaries, formatting, token and seam drift
checks, all four test projects, both localized builds, the backend suite, and the generated-artifact
drift gates. The full script list is in package.json; CLAUDE.md explains the traps.
Where atomic design pays off
Two orthogonal axes, and that is the point. On disk, code is grouped by ownership —
bounded context, then layer (domain/ → application/ → infrastructure/ → ui/, dependencies
pointing inward). In Storybook, the same components are grouped by atomic level — story titles
put them under Design System/Atoms|Molecules|Organisms|Templates. So libs/shared/src/ui/ is a
flat folder of 26 components, and the atomic ladder lives in the sidebar where you actually browse
it. See Atomic design and
Domain-driven design in Foundations.
Reuse is the payoff. button, form-field, async, page-shell, site-header appear on
essentially every screen across both apps. Change one, every screen follows.
A new page is composition, not new components. The branching intake wizard — the most complex
flow in the app — needed exactly one new atom (radio-group) and one new organism
(intake-wizard). Everything else was already there.
Theming is one stylesheet and a token bridge. libs/shared/styles.scss maps the app's
semantic --rhc-* vocabulary onto CIBG/--bs-* values, so components reference tokens, never
colours. Re-point the bridge to re-theme both apps with no component changes (ADR-0003).
npm run check:tokens fails the build on a hardcoded colour.
Try it: dev affordances
Append ?scenario= to any data page to force an async state — the states are mutually exclusive
by construction, via the <app-async> molecule:
| URL | What you see |
|---|---|
/dashboard |
real data |
/dashboard?scenario=slow |
skeletons for ~2.5s, then data |
/dashboard?scenario=loading |
the loading state, held open |
/dashboard?scenario=empty |
"geen gegevens" empty state |
/dashboard?scenario=error |
error message + Opnieuw proberen (retry) |
Append ?role=drafter|approver|admin to switch the dev role stand-in and unlock the admin pages.
Both toggles are dev-only — neither interceptor is wired into production builds.
Tech notes
- Angular 22 — standalone components, signals,
resource(), native control flow, view transitions. No NgRx: shared state is a root singleton store with a pure reducer. - Backend — ASP.NET Core (.NET 10), EF Core/SQLite for applications, documents, the brief and the audit trail; BRP/DUO reference data stays in-memory seeded. Screen-shaped ("BFF-lite") endpoints return server-computed decisions the frontend renders rather than recomputes (ADR-0001). Cases can be sourced from OpenZaak/ZGW behind the same seam (ADR-0005).
- Typed client — NSwag-generated from the backend's OpenAPI doc (
npm run gen:api); CI fails on drift. - Boundaries are enforced, not hoped for —
dep:checkfails the build ifdomain/imports Angular, a context imports upward, or an app reaches into the other app. - i18n — every user-facing string is
$localize-wrapped with a stable id (source localenl).ng build --localizebuilds bothnlanden, andi18nMissingTranslation: "error"fails the build if a string ships untranslated. - Dependencies — the shipped bundle audits clean (
npm audit --omit=dev: 0 vulnerabilities)..npmrcsetslegacy-peer-deps=truebecause Storybook's peer range lags Angular 22. Never runnpm audit fix --force— it downgrades Angular 22 → 21.
Deliberately out of scope
Real auth/DigiD, real BRP/DUO upstreams, a production-grade database, NgRx, licensed RO/Rijks
fonts and logo. The i18n build seam is proven, but the en translation is demo-quality and locale
is a build-time choice, not a runtime switch.