ehoandClaude Opus 5 2aa343f255 chore: cap container memory in docker compose
The API container used server garbage collection. Server GC makes one heap per
CPU, which is 22 heaps on this host. The two frontend containers sized the Node
heap from host RAM (16 GB), because compose sets no cgroup limit.

Set DOTNET_gcServer=0 and NODE_OPTIONS=--max-old-space-size=2048. This bounds
the memory that `docker compose up` needs.

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

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

S
Description
No description provided
Readme
5.5 MiB
Languages
TypeScript 61.6%
C# 24.2%
MDX 6.7%
JavaScript 2.2%
Shell 1.9%
Other 3.4%