Angular 22.1.x emits `var(--%NS%name)` for every CSS custom property in a component `styles:` block. No `@angular/core` release substitutes the placeholder, so all `--rhc-*` tokens resolve to nothing and the UI breaks. `npm run ci` does not catch it; only the Storybook axe job does. Pin every `@angular*` entry to the exact version 22.0.5, so a plain `npm install` cannot pull 22.1.x back in. Holding at 22.0.5 leaves three moderate advisories open, which made the audit step fail: GHSA-p297-fm68-3q8c and GHSA-hh8m-fm6v-7cvg. Neither is reachable. The app calls no `withRequestsMadeViaParent` and no `provideClientHydration`, and binds no untrusted value into a directive host binding. The audit gate therefore runs at `--audit-level=high`. A high advisory still fails the build. Restore the default audit level together with the upgrade, after an Angular release substitutes the placeholder. 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.