Files
atomic-design-poc/README.md
T
ehoandClaude Opus 5 12f17d9d73 docs: archive the finished backlogs (RD-30)
Two backlog trees are complete: `docs/project/backlog/` (75 files, every
WP done) and `docs/project/refactor-backlog-setup/` (the arc before it).
Move both under `docs/project/archive/` with `git mv`, so history stays
intact through `git log --follow`. `SHOWCASE-ROADMAP.md` moves with them,
because it points at the now-archived backlog README.

Add `docs/project/archive/README.md`. It states that these trees are
historical and names the two directories that are still live.

Repoint every inbound reference named in RD-30's Files table: CLAUDE.md,
the root README, both backend READMEs, `LetterHtml.cs`, `a11y.mdx`, the
`document-feature` and `new-ssp` skills, and the readable-codebase PLAN,
README, and RD-19 ticket. Fix two upward-relative links inside the moved
WP files (WP-68, WP-69) that gained a directory level and would otherwise
break. Repoint `.prettierignore`'s two agent-prompt exclusions to their
new path, so prettier keeps leaving those files' exact wording alone.

Mark RD-30 done and check off its acceptance criteria; flip its README
row to done.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 23:00:38 +02:00

10 KiB

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 / POCno 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 storybook and 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.md is 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 storybookFoundations → 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 fordep:check fails the build if domain/ 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 locale nl). ng build --localize builds both nl and en, and i18nMissingTranslation: "error" fails the build if a string ships untranslated.
  • Dependencies — the shipped bundle audits clean (npm audit --omit=dev: 0 vulnerabilities). .npmrc sets legacy-peer-deps=true because Storybook's peer range lags Angular 22. Never run npm 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.