Files
register-referentie/docs/frontend-decisions.md
Niek Otten b00d8bd60e
All checks were successful
CI / lint (pull_request) Successful in 1m14s
CI / build (pull_request) Successful in 54s
CI / unit (pull_request) Successful in 1m4s
CI / frontend (pull_request) Successful in 1m55s
CI / mutation (pull_request) Successful in 4m1s
CI / verify-stack (pull_request) Successful in 6m57s
docs(portal-openbaar): record openbaar decisions + walking-skeleton public-visibility demo (refs #10)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:16:30 +02:00

8.2 KiB

Frontend decisions

A running log of frontend tooling and component decisions (CLAUDE.md §10). One entry per decision; record why, and note any deviation from NL Design System.


Workspace & tooling (S-08a, #65)

The portals live in an Nx monorepo at the repository root, alongside the .NET services/.

  • Package manager: pnpm. Native build scripts are approved explicitly in pnpm-workspace.yaml under allowBuilds (pnpm 11 fails the install otherwise). Node 24, pnpm 11.
  • Angular, standalone components + signals, no NgModules (§10). Apps are generated with @nx/angular:application.
  • Unit tests: Vitest via Angular's built-in @angular/build:unit-test (the vitest-angular runner). Angular Testing Library is added for component tests when the first real components land (S-08c); the S-08a placeholder uses a plain TestBed render assertion.
  • Lint: ESLint (flat config, @nx/eslint).
  • Nx is scoped to apps/ + libs/ only. The @nx/docker and @nx/dotnet plugins are not installed — the .NET services are built by dotnet/the Makefile, and @nx/docker would otherwise infer every services/*/Dockerfile as an unnamed Nx project and break the project graph.
  • No Nx Cloud. nxCloudId is stripped from nx.json; remote caching would depend on an external service, and the repo is Gitea-only (§8.7). Nx's "configure-ai-agents" additions (.claude/settings.json, a CLAUDE.md section referencing a GitHub marketplace) are not committed for the same reason.
  • CI: a frontend job (make frontendpnpm install --frozen-lockfile + nx run-many -t lint test build) runs on pnpm + Node, with pinned action URLs (§15).

NL Design System: not yet introduced — the S-08a app is a placeholder. NL DS components arrive with the submit form (S-08c, #67); any deviation from NL DS will be recorded here.


API client generator (S-08b, #66)

libs/api-client is generated from services/bff/openapi.json — never hand-written (§10).

  • Generator: orval (client: 'angular'), a node-based generator (no Java, unlike openapi-generator), so it runs in the pnpm/Node CI lane. It emits an injectable BffApiV1Service using Angular's HttpClient — which means the DigiD bearer token can be attached by an HttpInterceptor (S-08c), the idiomatic Angular approach; a fetch-based SDK would bypass the interceptor pipeline.
  • Config: libs/api-client/orval.config.ts (single-file output into src/lib/generated/, clean: true, prettier). Regenerate with nx run api-client:generate after the BFF spec changes; the output is deterministic (idempotent), and src/lib/generated/ is never hand-edited.
  • Tested against a mocked BFF via HttpClientTesting (libs/api-client/src/lib/bff-api.spec.ts).
  • The BFF endpoints carry no operationId, so orval synthesises method names (postSelfServiceRegistrations, getOpenbaarRegister); adding explicit operation ids to the BFF is a possible later polish.

Self-service form: NL DS, DigiD auth, testing (S-08c, #67)

  • NL Design System via @utrecht/component-library-angular (libs/ui) + @utrecht/design-tokens (imported once in apps/self-service/src/styles.css). Utrecht is NL DS's reference Angular implementation. Its v3 components are NgModule-based, not standalone, so libs/ui re-exports UtrechtComponentsModule (and the component classes, so the AOT compiler resolves the template directives through the barrel); standalone components consume it via imports: [UtrechtComponentsModule]. §10's "no NgModules in new code" governs our code — consuming a third-party module is fine.
  • DigiD login via angular-auth-oidc-client (libs/auth): auth-code + PKCE against the Keycloak digid realm (public client big-portal). A small AuthService abstraction (bsn / isAuthenticated / login) wraps the library so components and the authenticatedGuard depend on a mockable surface; a token HttpInterceptor attaches the bearer to BFF calls (secure route). The OIDC authority/secureApiOrigin are dev defaults in app.config.ts; the compose-served app overrides them (S-08d), and the browser-vs-container issuer alignment is handled there (ADR-0010).
  • Testing: component tests use @testing-library/angular (§10) with AuthService and the api-client mocked; the axe (vitest-axe) check runs scoped to WCAG 2.1 AA tags (wcag2a/2aa/21a/21aa) with the document lang set, asserting zero violations on the submit page. The real DigiD browser round-trip is exercised in S-08d (Playwright).
  • Module boundaries: replaced the demo eslint depConstraints (scope:shop/scope:shared, left over from the Nx angular template) with a permissive * default; scope/type tags can be introduced when the portal set grows.

Serving + e2e (S-08d, #68)

  • Served by nginx, same-origin as the BFF. The compose self-service image serves the built app and reverse-proxies /self-service/* + /openbaar/* to the bff service. Because the api-client uses relative URLs, the browser calls the app's own origin → nginx forwards to the BFF: no CORS, and the DigiD token (same-origin) is attached by the interceptor. nginx resolves the BFF at request time (a resolver + variable proxy_pass) so it starts before the BFF is up.
  • Runtime config. The app fetches /config.json before bootstrap (main.ts); appConfig is a factory. The dev default (public/config.json) points at localhost:8180; the Docker image bakes the compose value (keycloak:8080). One build, per-environment OIDC authority.
  • e2e runs inside the compose network. infra/run-e2e-check.sh runs Playwright in a container on cg, so the browser reaches Keycloak as keycloak:8080 — the same issuer the BFF validates against (resolves the browser-vs-container mismatch, ADR-0010). It uses the official mcr.microsoft.com/playwright:<version> image with browsers pre-baked, rather than downloading ~150 MB of Chromium on every run (issue #73) — the image tag is kept in lockstep with tests/e2e/package.json's @playwright/test version. The spec is copied in (docker cp), not mounted, so it leaves nothing root-owned on the host. Wired as verify-e2e in the verify-stack CI job.
  • e2e treats the portal origin as secure. In-network the portal is served over plain HTTP on a non-localhost origin (http://self-service), which is not a secure context, so Web Crypto (crypto.subtle) is unavailable. angular-auth-oidc-client needs it for the PKCE code challenge, so authorize() throws and the login redirect never fires. Production runs behind HTTPS where this is a non-issue; rather than terminate TLS in the throwaway stack, the Playwright config passes --unsafely-treat-insecure-origin-as-secure (honoured only by the full channel: 'chromium' build, not the default headless-shell). This emulates the production HTTPS secure context without touching the app or its production config.
  • tests/e2e is a standalone Playwright project (its own package.json), not an Nx project — it's a live-stack check like the other verify-* runners, not part of the frontend unit lane.

Openbaar Register portal (S-09, #10)

  • Anonymous, no auth. The openbaar register is a public read, so apps/openbaar has no angular-auth-oidc-client, no interceptor, and no config.jsonmain.ts bootstraps appConfig directly with just provideHttpClient + provideRouter. This is the deliberate contrast to self-service and keeps the app trivially cacheable/CDN-able.
  • Same-origin via nginx, like self-service. The compose openbaar image serves the built app and reverse-proxies /openbaar to the BFF; the api-client's relative calls stay same-origin (no CORS). Served on :8141, health-checked over IPv4 (127.0.0.1), no Keycloak dependency.
  • Public-safe by construction. The portal only ever sees the BFF's OpenbaarProjection.PublicView (id + status); bsn/naam never leave the BFF. The e2e asserts the bsn never renders.
  • Loads on open, filters on search. RegisterPage fetches the full register on construction and re-queries /openbaar/register?q= on search — no client-side filtering, the BFF owns the query.