Files
apf_portal/CLAUDE.md
T
julien c080d1ad89
CI / scan (push) Successful in 3m8s
CI / commits (push) Has been skipped
CI / check (push) Successful in 3m58s
CI / a11y (push) Successful in 4m38s
Docs site / build (push) Successful in 7m47s
CI / perf (push) Successful in 8m53s
feat(infra): dockerised full-stack dev mode — apps compose profile (ADR-0030) (#258)
## Summary

Implements [ADR-0030](../docs/decisions/0030-dockerised-dev-mode.md) (now `accepted`): a Docker Compose `apps` profile that runs the three Nx dev servers (`portal-bff`, `portal-shell`, `portal-admin`) from a shared `Dockerfile.dev`, so a developer can boot the whole stack with **no native Node/pnpm**:

```bash
./infra/local/dev.sh up apps   # infra + portal-bff:3000 + portal-shell:4200 + portal-admin:4300
```

Purely additive and profile-gated — the native `nx serve` flow and the devcontainer are untouched. Dev-only; no production images (those stay with the ADR-0028 Container Registry work).

## What lands

| File | Change |
| --- | --- |
| `docs/decisions/0030-dockerised-dev-mode.md` | Status `proposed` → `accepted`. |
| `docs/decisions/README.md` | Index status → `accepted`. |
| `infra/local/Dockerfile.dev` | **New.** `node:24-bookworm` + corepack (pnpm resolved from `packageManager` at runtime — no pinned version to drift). No COPY/install at build time. `NX_DAEMON=false`, `NODE_OPTIONS=--max-old-space-size=4096`. |
| `infra/local/dev-entrypoint.sh` | **New.** Shared entrypoint: BFF (`APF_ROLE=bff`) runs `prisma generate` + `prisma migrate deploy` then serves; SPA services go straight to `nx serve`. |
| `infra/local/dev.compose.yml` | **New `apps` profile.** A one-shot `apps-deps` service installs into a shared `node_modules` volume once (the 3 servers gate on its `service_completed_successfully`, avoiding a 3-way install race); `portal-bff` / `portal-shell` / `portal-admin` services from the shared image via a `x-app-base` anchor. Repo bind-mounted; `node_modules` + `.nx` in named volumes. |
| `infra/local/dev.sh` | `apps` added to `ALL_PROFILES` (so teardown / status / logs catch it) + usage / examples. |
| `infra/README.md` | New "Dockerised app dev mode" section + cheat-sheet / file-table rows. |
| `docs/setup/01-dev-debian-vm-setup.md` | "Three dev modes — which when" table at the top of Step 5. |
| `CLAUDE.md` | Architecture roll-up bullet + ADR-count line + environment-conventions note. |

## Key design decisions

- **One image, one install.** The monorepo means a single `Dockerfile.dev` + a single `pnpm install` serves all three apps.
- **`node_modules` + `.nx` in named volumes, not bind-mounted.** The container's install (native modules — `esbuild`, `@swc/core`, Prisma engines, `lmdb`, `@parcel/watcher` — built for this image) must never be shadowed by the host's `node_modules`. The repo source is bind-mounted for hot reload; these two directories are overlaid with named volumes.
- **`apps-deps` one-shot avoids the install race.** Three services sharing one `node_modules` volume can't all run `pnpm install` concurrently. A dedicated install service runs first; the three app services `depends_on` its completion.
- **`NX_DAEMON=false`** in the containers — three containers sharing one workspace would otherwise contend on the Nx daemon.
- **Env wiring.** The BFF reuses its own `apps/portal-bff/.env` (Entra / session / jwks secrets) via `env_file: { required: false }`; the host-specific URLs (`DATABASE_URL` / `REDIS_URL` / OTel endpoint) are overridden in `environment:` — rebuilt from `infra/local/.env` creds → Compose service names. Compose `environment` wins over `env_file`, so the localhost values in the BFF `.env` don't leak into the container.
- **BFF still needs its secrets.** "No native toolchain" ≠ "no config". `apps/portal-bff/.env` must exist (same as native dev); `required: false` lets SPA-only devs `up` without it (the BFF then fails its own boot validators with a clear message).

## Validation on the VM

- [x] `docker compose -f dev.compose.yml --profile apps config` validates (YAML, anchors / merge, env interpolation).
- [x] `bash -n` clean on `dev-entrypoint.sh` and `dev.sh`.
- [x] **Full boot on vm-dev** — `./infra/local/dev.sh up apps` brings up postgres / redis / otel + `apps-deps` (one-shot, exit 0) + portal-bff / portal-shell / portal-admin, all containers report healthy or running.
- [x] `apps-deps` populates the shared `node_modules` volume; the three servers reach their `nx serve` step without re-installing.
- [x] Ports published as expected: BFF :3000, portal-shell :4200, portal-admin :4300.
- [x] `./infra/local/dev.sh up` (no `apps`) unchanged for native devs.

## Follow-ups identified during VM validation

- **SPA → BFF reachability from a remote browser.** Opening `http://<vm-ip>:4200/` from the workstation surfaces a "Backend unreachable" message: the SPA's hardcoded `bffApiBaseUrl: 'http://localhost:3000/api'` (ADR-0018 build-time env) plus the BFF's `CORS_ALLOWED_ORIGINS=http://localhost:4200,…` both assume "browser on the same machine as the BFF", which doesn't hold here. Fixed in the **stacked follow-up PR `feat/spa-dev-proxy`** (proxy `/api` in the Angular dev-server + relative `bffApiBaseUrl`), which lands right after this PR.
- The OTel HTTP exporter URL (`environment.otlpEndpoint`) and the cross-SPA links (`adminAppUrl`, `shellAppUrl`) remain absolute and hit the same remote-browser limit; not blocking for v1, can be revisited if needed.

## Related

- [ADR-0030](docs/decisions/0030-dockerised-dev-mode.md) — the decision (accepted in this PR's chain).
- [ADR-0020](docs/decisions/0020-portal-admin-app.md) — the devcontainer this complements.
- [ADR-0028](docs/decisions/0028-migrate-cicd-and-git-hosting-to-gitlab.md) — production images / Container Registry (deferred).
- Follow-up branch `feat/spa-dev-proxy` — the SPA-side proxy fix that makes the dockerised mode usable from a remote browser.

---------

Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #258
2026-06-01 11:11:44 +02:00

27 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project rules (durable)

These constraints were set by the project lead at kickoff. They apply to every change.

  • Scale & quality bar. Treat this as a large-scale portal for a sizable organization, not a prototype. No bricolage, no exotic stacks. Default to stable, recognized, battle-tested choices. Cutting-edge / "à la pointe" alternatives must always be evaluated alongside the stable option, but are only adopted when the trade-off is captured in an ADR (drivers, risk, exit strategy). Pre-1.0 dependencies and one-maintainer projects are rejected unless an ADR justifies the exception.
  • Security, performance, accessibility. All three are first-class concerns from day one — never bolted on. Architecture, dependency, and feature decisions must explicitly consider their impact on these axes and document the trade-offs.
  • Project name. Currently apf_portal, provisional. Do not hardcode it outside repo/workspace-level metadata so a rename stays a one-line change.
  • Language. All code, identifiers, comments, documentation, commit messages, and PR descriptions are written in English. (Conversation with the project lead happens in French — but artifacts shipped in the repo are English-only.)
  • Commits / PRs. Never add a Co-Authored-By: Claude trailer or a 🤖 Generated with Claude Code footer to commits or PR bodies.
  • Be a peer, not a typist. Challenge requests when a better approach exists; surface trade-offs frankly. Don't silently execute a suboptimal directive — propose, then execute the agreed plan.

Documentation

  • All documentation lives in .md files under docs/, indexed by docs/README.md. The index is maintained automatically whenever a doc is added, renamed, or removed — no need to be asked.
  • Documentation is written proactively whenever it is genuinely useful (architecture, runbooks, onboarding, security/perf/a11y rationales). It is not created for trivial things just to tick a box.
  • The folder notes/ is the project lead's personal scratchpad — git-ignored and not part of project artifacts. Never write project documentation there.

Architectural Decision Records (ADRs)

  • Format: MADR 4.0.0 (https://adr.github.io/, https://github.com/adr/madr). Template at docs/decisions/template.md.
  • Location: flat folder docs/decisions/, indexed by docs/decisions/README.md.
  • Filename convention: NNNN-kebab-title.md with globally sequential 4-digit numbers. Numbers are never reset and never reused — even when an ADR is superseded or deprecated.
  • Categorization: via the tags: array in the MADR frontmatter (e.g. [frontend, security]). The canonical tag vocabulary lives in docs/decisions/README.md; never invent ad-hoc tags inline.
  • Proactivity. Any non-trivial development decision (tool/library choice, framework pattern, security control, perf budget, a11y target, naming convention, deprecation, breaking change) warrants proposing an ADR before implementation. Don't wait to be asked. Update the index in the same change.

Architecture (recorded in ADRs)

The structural, security, observability, and quality choices are recorded as ADRs and summarized below. Any change to these requires updating the corresponding ADR.

  • Workspace: Nx monorepo with the apps preset, managed by pnpm — see ADR-0002.
  • Naming: workspace apf-portal; apps portal-shell (end-user SPA), portal-admin (admin SPA, skeleton in place — see ADR-0020), and portal-bff (backend); libs feature-<name> and shared-<scope> — see ADR-0003.
  • Frontend (portal-shell): Angular at the latest LTS major — standalone APIs, zoneless change detection, Signals, CSR only (no SSR), Vitest, SCSS — see ADR-0004.
  • Backend (portal-bff): NestJS at the latest stable major, mounted on the Express adapter (Fastify adapter swappable later) — see ADR-0005.
  • Persistence: PostgreSQL (latest stable major) via Prisma — see ADR-0006.
  • Sessions: opaque session id in __Host-portal_session, payload in self-hosted Redis (Sentinel HA in prod, single node in dev), tokens encrypted at rest with AES-256-GCM, idle 30 min sliding + absolute 12 h — see ADR-0010.
  • MFA: enforced by Entra ID Conditional Access (org-side policy, P1 licensing required); BFF sanity-checks the amr claim at session creation; @RequireMfa() decorator and freshness-based step-up are designed-in for future sensitive routes (no v1 consumer) — see ADR-0011.
  • Identity: multi-tenant Microsoft Entra ID with B2B invitation for workforce in v1, dual-audience design ready for future External ID activation — see ADR-0008.
  • Authentication flow: OIDC Authorization Code + PKCE via @azure/msal-node, executed entirely on the BFF; SPA never holds tokens; __Host- prefixed cookies, double-submit CSRF, RP-initiated logout — see ADR-0009.
  • Observability: Pino + nestjs-pino for structured JSON logs, OpenTelemetry SDK + auto-instrumentations for traces, W3C Trace Context propagation across SPA → BFF → DB → Redis, nestjs-cls for request-scoped context (trace_id, session_id, user_id_hash, audience), 100 % sampling at the app with tail sampling deferred to the OTel Collector, stdout + OTLP shipping — see ADR-0012.
  • Audit trail: dedicated audit.events schema in the same Postgres instance, append-only by Postgres role grants (audit_writer INSERT, audit_reader SELECT, audit_archiver DELETE older than retention; no UPDATE/TRUNCATE to anyone); 365-day retention default; cross-referenced with app logs via trace_id and actor_id_hash (same salt); blocking writes (no audit ⇒ no action) — see ADR-0013.
  • Downstream API access: unified DownstreamApiClient (@nestjs/axios + cockatiel), per-service DownstreamApiConfig; default auth strategy is OBO via MSAL Node for Entra-protected APIs (downstream-scoped tokens cached in Redis with AES-256-GCM under a dedicated key); fallback strategy is service credential + signed X-User-Assertion JWT (BFF JWKS at /.well-known/jwks.json); per-call audience pre-check; no axios/fetch outside src/downstream/ — see ADR-0014.
  • CI/CD: Gitea Actions today, migrating to GitLab CE on vm-gitlab per ADR-0028 (decision-only ADR accepted; 4-phase rollout in follow-up PRs — the architectural principles below carry over unchanged from ADR-0015, only the host / pipeline file / runner type / scan tooling change). Trunk-based with squash-merge, branch protection on main, all CI gates blocking. Thin YAML — orchestration logic lives in package.json scripts (ci:check, ci:catalogue-drift, ci:audit, ci:commits, ci:perf, ci:gzip-budgets) and Nx targets, runnable locally. Gates: format / lint / type-check / test / build / audit / secret-scan / commit-lint, plus a11y (per ADR-0016) and perf (per ADR-0017). Self-hosted runners on-prem (act_runner today, GitLab Runner with Docker executor post-migration). Conventional Commits validated locally (hook) and in CI (defense in depth). Required reviewer count = 0 in v1, raised to ≥1 once a second contributor joins. Signed commits required on main once the migration cutover lands (ADR-0028's revisit of ADR-0015's "recommended" stance).
  • Accessibility: WCAG 2.2 AA baseline + targeted AAA on criteria with high impact for APF's user base (1.4.6 Contrast Enhanced, 2.2.3 No Timing, 2.3.3 Animation, 3.1.5 Reading Level, 1.4.8 Visual Presentation, 2.4.9 Link Purpose, 3.3.5 Help). RGAA 4.1 alignment for French audit. UI stack: Angular CDK + TailwindCSS (spartan-ng library deferred until it reaches 1.0.0; v1 components are written in-house in libs/shared/ui/ on Angular CDK, applying the spartan-ng philosophy of headless primitives + utility CSS + copy-paste). User-preferences panel (contrast / text size / motion / spacing / cognitive simplification / reading focus) persisted in session. Tooling: @angular-eslint/template/* lint, @axe-core/playwright e2e (blocking on critical/serious), token-contrast CI check, touch-target check (44×44 min). Manual testing cadence with APF's internal user panel before each major release. Public accessibility statement page at /accessibility and /accessibilite — see ADR-0016.
  • Performance budgets: Core Web Vitals at Google "Good" thresholds (LCP ≤ 2.5 s, INP ≤ 200 ms, CLS ≤ 0.1, TBT ≤ 200 ms, TTFB ≤ 800 ms), Lighthouse Performance ≥ 90 on critical routes. Lighthouse CI (@lhci/cli) runs in CI with median-of-3 mitigation, blocking on threshold breach. Angular bundle budgets (type: "error"): initial ≤ 300 KB gzip, lazy chunks ≤ 100 KB gzip. BFF p95/p99 SLOs per endpoint family observed via OTel (advisory in CI, alerting in prod). Weekly scheduled Lighthouse run on prod env. a11y wins over perf when they conflict — see ADR-0017.
  • Environment configuration: SPA per-environment values via Angular environment.ts + fileReplacements at build time (no runtime config-fetch). BFF reads process.env directly with small per-key boot-time validators (no @nestjs/config overhead at this scale). The audit log uses a separate AUDIT_DATABASE_URL connection pool in production (audit_writer-only login, defense in depth) and falls back to the shared pool + SET LOCAL ROLE in dev — see ADR-0018.
  • Internationalisation: @angular/localize in build-time mode, two locales (fr default served at /, en), source locale = English (project English-only rule). Path-based URLs always prefixed (/fr/..., /en/...); / smart-redirects via cookie → Accept-Languagefr. UI strings live in XLIFF (messages.fr.xlf); editorial / CMS content is BFF-served already localised (see admin app). Footer hosts the locale switcher; switching writes a __Host-portal_locale cookie and hard-refreshes — see ADR-0019.
  • Admin application (portal-admin): dedicated Angular SPA alongside portal-shell, sharing the same portal-bff via /api/admin/* routes guarded by an Entra Portal.Admin role + @RequireMfa({ freshness: 600 }) at entry. Distinct origin / cookie / session from portal-shell (__Host-portal_admin_session). v1 modules: CMS for static pages (multilingual), menu management, user list (read-only), audit log viewer. Bundle budget relaxed to ≤ 500 KB gzip (vs 300 KB for portal-shell); same a11y + dark-mode baseline. Shared UI primitives (Icon, LayoutStateService, brand tokens) graduate to libs/shared/* as both apps need them — see ADR-0020.
  • Local quality gates: Husky + lint-staged + commitlint with Conventional Commits — see ADR-0007.
  • Documentation site: docs/**/*.md rendered as a separate static site via VitePress (Vite-based, Node-only toolchain, Markdown-first). Mermaid diagrams via vitepress-plugin-mermaid. Deployed on its own hostname behind the shared reverse-proxy; CI hook on docs/ changes rebuilds + publishes. Decoupled from the apps — content lives in docs/, no in-app Markdown viewer — see ADR-0022.
  • Charts + dashboards: D3 + Observable Plot wrapped in libs/shared/charts/, one Angular component per chart type (bar, donut, line, stacked-bar, …). A11y baked in by the lib (SVG <title>/<desc>, <details> tabular fallback, colour-blind-safe palettes, AA-contrast text, prefers-reduced-motion gate). Bundle stays under ADR-0017's lazy-chunk cap via per-d3-* module tree-shaking. Future bespoke visualisations land in raw D3 inside the same lib — see ADR-0023.
  • AI service relay: dedicated apf-ai-service repo (ASP.NET Core, Microsoft Agent Framework) consumed via native gRPC HTTP/2 only — proto contract vendored under apps/portal-bff/src/grpc/proto/apf-ai/ with ts-proto codegen committed alongside. BFF dials with @grpc/grpc-js (h2c in dev, h2 + TLS in prod), bridges ChatService.Chat to text/event-stream for the SPA, exposes RagService.Search and ModelsService.ListModels as plain JSON endpoints. Identity travels as an unsigned Principal (subject, roles, attributes) in the proto body for the POC, hashed via the audit module's HashUserIdService so portal and AI service audit trails join on the same actor_id_hash. Production hardening (signed envelope vs mTLS) deferred — see ADR-0024.
  • Authorization model: three orthogonal axes — privileges (Entra app roles, Portal.*), functional roles (Entra security groups → curated apf-role-* slug catalogue, 24 entries v1), scopes (portal-side user_scopes table, future Pléiades feed; kinds = self / etablissement:<structure-code> / delegation:<dept> / region:<insee> / siege / unrestricted, see ADR-0027 for the Structure.code semantics). Composed at sign-in into a session-resident Principal; portal guards consume the structured shape, a deterministic PrincipalProjector flattens it to the AI-service roles[] contract. Replaces stargate's linear hierarchy. Catalogues are closed-set, drift gated by CI — see ADR-0025.
  • Portal-side identity model: Person golden record (stable identity, can exist without a portal account — workforce pre-provisioning, dossier bénéficiaires, alumni) + User overlay (one-to-zero-or-one with Person, lazy-created on first OIDC callback in v1; carries portal-only state like lastSignInAt). UserScope backs the ADR-0025 scope axis with opaque value strings referencing ADR-0027's Structure.code / Delegation.code / Region.code — no FK at the DB level so historical rows survive structure decommissioning; admin-UI write path validates. v1 dedup uses entraOid only; Person.email is an indexed attribute, not a unique key, because two distinct humans genuinely share emails (shared aliases, generic info@, upstream-feed errors). Facets (Salarié / Élu / Adhérent / Bénéficiaire) + Pléiades / Acteurs+ sync + operator-confirmed Person-merge flow deferred to ADR-0029 — see ADR-0026.
  • Portal-side organisational hierarchy: Region (INSEE 2-digit) → Delegation (department 23-char) → Structure with kind discriminator (medico_social / antenne / dispositif / entreprise_adaptee / mouvement / administratif / siege, aligned with cascade's Structure.type). Structure.code is the portal-internal string PK, externally meaningful: for medico-social rows it equals the FINESS (9 digits) and round-trips cleanly through scope literals (etablissement:0330800013) and URLs; for non-FINESS rows it is an APF-internal slug (siege, apf-bdx-merignac, ea-toulouse, …). finess / siret / codePaie are nullable, unique-when-present attributes — populated where the upstream registry has the structure on file. v1 ships a small inline-migration seed (test-tenant scope: Région Nouvelle-Aquitaine, Délégation 33, a handful of médico-social + siège); the full cascade-driven inventory sync, plus Pole / Service / arbitrary nesting / per-source enrichment, land additively with ADR-0029 — see ADR-0027.
  • Runtime: Node.js latest LTS major.
  • Local dev environment: three coexisting run modes — native pnpm nx serve, the VSCode devcontainer, and a Docker Compose apps profile that boots all three Nx dev servers without a native Node/pnpm toolchain (./infra/local/dev.sh up apps). Dev-only (production images deferred to the ADR-0028 Container Registry work); shared Dockerfile.dev, repo bind-mounted for hot reload, node_modules/Nx cache in named volumes, BFF entrypoint runs prisma generate + migrate deploy — see ADR-0030.

Repository status

The Nx workspace is scaffolded and operational. The three apps (portal-shell, portal-admin, portal-bff) and the seven lib roots (libs/feature/auth, libs/shared/auth, libs/shared/charts, libs/shared/state, libs/shared/tokens, libs/shared/ui, libs/shared/util) are in place; CI runs format:check / lint / test / build plus the ADR-0025 catalogue-drift gate on every PR.

ADRs 0001 → 0028 plus ADR-0030 are accepted and cover the structural, security, observability, quality, i18n, admin-app, docs-site, charts, AI-relay, authorization, portal-side identity + organisational hierarchy, CI/CD platform migration, and dockerised local-dev mode choices. ADR-0028 supersedes only ADR-0015's "Gitea Actions" platform choice — the rest of ADR-0015's architectural principles carry over unchanged; the 4-phase migration (mirror → parallel pipelines → cutover → cleanup) ships in follow-up PRs. ADR-0029 (cascade / Pléiades / Acteurs+ syncs + facet schemas) is the next proposed addition — its number is reserved ahead of ADR-0030, which was written first. Until ADR-0026 + ADR-0027 implementation PRs ship, ADR-0025's stubs (Principal.user.{id, personId} placeholders, StubScopeResolver's unrestricted blanket return) remain in place. Shipped on main:

  • Phase-1 foundation — Nx workspace, Angular portal-shell, NestJS portal-bff, Prisma + Postgres, Pino + OpenTelemetry, Husky/lint-staged/commitlint, Gitea Actions CI.
  • Phase-2 auth + audit + security — OIDC Auth Code + PKCE via MSAL Node, Redis sessions with AES-256-GCM at rest, idle 30 min sliding + absolute 12 h hard ceiling, RP-initiated logout, double-submit CSRF, audit.events append-only schema with role-based grants, helmet + env-driven CORS allowlist + rate limiting + structured error envelope (see ADR-0021).
  • Phase-3a admin appportal-admin SPA with brand tokens, routing, user-list reader (/admin/users), and audit-log viewer with statistics and integrated charts (/admin/audit). CMS for static pages and menu management not yet implemented.
  • AI relay surface + live consumer — vendored protos + AiClientModule (gRPC clients, Principal mapper, metadata builder) + AiBridgeController exposing POST /api/ai/chat (SSE), GET /api/ai/rag/search, GET /api/ai/models (see ADR-0024). Chatbot widget live in portal-shell at apps/portal-shell/src/app/features/chatbot/.
  • Docs static site (ADR-0022) — VitePress + Mermaid renderer at docs/.vitepress/, dedicated docs-site.yml workflow that rebuilds + publishes on every docs/** change.
  • Charts lib + audit-page dashboards (ADR-0023) — libs/shared/charts/ with BarChart, DonutChart, StackedBarChart (D3 + Observable Plot, headless / a11y-baked-in), integrated into the /admin/audit page for daily-volume + outcome-breakdown + event-type-over-time views.
  • Authorization model + guards (ADR-0025) — libs/shared/auth/ exporting the closed catalogues (4 privileges, 24 functional roles, 6 scope kinds), Principal shape, pure matchers, and EntraGroupToRoleResolver. BFF-side PrincipalBuilder composing the three axes at sign-in from Entra roles + groups claims + a stub ScopeResolver. @RequirePrivilege / @RequireRole / @RequireScope route decorators + guards with the ADR-0021 structured-error envelope on denial; AdminRoleGuard migrated to read principal.privileges. CI drift gate (scripts/check-catalogue-drift.mjs) asserting every decorator literal is in the catalogue.

Still on the roadmap:

  • DownstreamApiClient + OBO (ADR-0014) — module scaffolded (obo.strategy, signed-assertion.strategy, JWKS publisher, encrypted token cache); no v1 consumer yet. Wires in when the first business route needs an Entra-protected API.
  • @RequireMfa() step-up consumer routes (ADR-0011) — guard + decorator shipped; awaiting first sensitive route that needs explicit freshness enforcement beyond the Conditional Access baseline.
  • @RequireScope Prisma-backed resolver + first consumer surfaceStubScopeResolver returns unrestricted for everyone in v1 per ADR-0025 §331. Implementation lands across ADR-0026 (accepted: Person + User + UserScope) and ADR-0027 (accepted: Region / Delegation / Structure with kind discriminator + nullable FINESS / SIRET). Sequencing: ADR-0026 PR 1 + ADR-0027 PR 1 ship schema in parallel; ADR-0026 PR 2 then lands the PrismaScopeResolver + admin scope-seeding UI + test-tenant seed (which references ADR-0027's Structure.code values). The follow-up ADR-0029 covers Pléiades / Acteurs+ / cascade syncs + facet schemas.
  • Proto-drift CI gate for the AI relay (ADR-0024) — asserts the vendored apf-ai-service proto files stay in lockstep with the upstream contract.
  • Admin app — CMS & menu management (ADR-0020) — multilingual static-page editor + navigation menu builder. The user-list + audit-log-viewer modules already exist; the CMS/menu pair is the remaining v1 module scope.
  • Strategic security baseline ADR — separate from the implementation-level ADR-0021. Remains paused awaiting RSSI input on the OWASP ASVS reference level and adjacent frameworks (HDS, GDPR, possibly NIS 2). When it lands it will either confirm 0021 or supersede pieces of it.

Commands once the workspace exists

App-scoped — <app> is one of portal-shell, portal-admin, portal-bff:

pnpm nx serve <app>      # dev server
pnpm nx build <app>
pnpm nx test <app>       # Vitest, all tests for the app
pnpm nx lint <app>

Run a single test file:

pnpm nx test <app> --testFile=path/to/file.spec.ts

Workspace-wide:

pnpm nx run-many -t lint test build
pnpm nx affected -t lint test build   # only projects affected by current changes
pnpm nx format:check

Environment conventions

  • Two development environments. local (Windows-WSL or native macOS / Linux on the workstation) and development (Debian 13 VM at 10.100.201.21 — the default for new devs, replaces WSL). A hybrid sub-mode runs the IDE + Nx servers on the workstation while reaching the infra services (postgres / redis / otel) on the dev VM through SSH tunnels. Full procedure: docs/setup/01-dev-debian-vm-setup.md. The legacy WSL flow remains documented in docs/setup/02-wsl-terminal-setup.md.
  • Two IDE flows on the dev VM — VSCode Remote-SSH (default; transparent equivalent of the WSL flow) and Devcontainer (.devcontainer/devcontainer.json shipped, Node + pnpm pinned in the image, mounts the docker socket so the host's apf-portal-dev Compose network is reachable from inside). Independently of the IDE flow, the apps can run natively (pnpm nx serve) or as Docker Compose services (./infra/local/dev.sh up apps, no native toolchain — ADR-0030); the "which mode when" table is in docs/setup/01-dev-debian-vm-setup.md.
  • Never install Angular globally. Use pnpm dlx for one-off CLI invocations and project-local pnpm nx ... for everything else — versions stay pinned per project.
  • On WSL: work inside the WSL filesystem (~/Works/...), never under /mnt/c — the latter has severe I/O penalties that break Nx caching and dev-server reload times. On the dev VM the analogous rule is "stay on the VM disk, do not work over SSHFS / network mounts".
  • pnpm is mandatory (activated via corepack enable); do not introduce npm or yarn lockfiles.
  • Prettier config target: singleQuote: true, semi: true, printWidth: 100.

General Guidelines for working with Nx

  • For navigating/exploring the workspace, invoke the nx-workspace skill first - it has patterns for querying projects, targets, and dependencies
  • When running tasks (for example build, lint, test, e2e, etc.), always prefer running the task through nx (i.e. nx run, nx run-many, nx affected) instead of using the underlying tooling directly
  • Prefix nx commands with the workspace's package manager (e.g., pnpm nx build, npm exec nx test) - avoids using globally installed CLI
  • You have access to the Nx MCP server and its tools, use them to help the user
  • For Nx plugin best practices, check node_modules/@nx/<plugin>/PLUGIN.md. Not all plugins have this file - proceed without it if unavailable.
  • NEVER guess CLI flags - always check nx_docs or --help first when unsure

Scaffolding & Generators

  • For scaffolding tasks (creating apps, libs, project structure, setup), ALWAYS invoke the nx-generate skill FIRST before exploring or calling MCP tools

When to use nx_docs

  • USE for: advanced config options, unfamiliar flags, migration guides, plugin configuration, edge cases
  • DON'T USE for: basic generator syntax (nx g @nx/react:app), standard commands, things you already know
  • The nx-generate skill handles generator discovery internally - don't call nx_docs just to look up generator syntax