## Summary Adds a full Debian 13 dev-VM setup procedure ([docs/setup/01-dev-debian-vm-setup.md](docs/setup/01-dev-debian-vm-setup.md)) + 10 modular idempotent setup scripts + a systemd template + a `.devcontainer/` spec, in preparation for the new dev VM (`10.100.201.21`) replacing the WSL-based workflow. Both IDE flows (VSCode Remote-SSH + Devcontainer) and both Node toolchains (nvm on host + devcontainer image) are available — devs pick per task. Adjacent context (does not ship here, planned follow-up): - **Preview infra on the GitLab VM (`10.100.201.10`)** — same `dev.compose.yml`, deployed by CI on `main`. Doc placeholder in §8.6. - **GitLab Runner migration** (act_runner Gitea → GitLab Runner Docker executor) — bundled with the Gitea → GitLab cutover. - **Private dotfiles repo** (`apf/dotfiles`) — `~/.zshrc`, `~/.p10k.zsh`, `~/.tmux.conf` versioned. `80-dotfiles.sh` is already structured to fall back to a `~/.dotfiles/` clone when present. No application-code changes. No CI gate impact (doc + scripts + devcontainer spec only). ## What lands | Path | Change | | --- | --- | | `docs/setup/01-dev-debian-vm-setup.md` | **New.** Step-by-step doc: workstation prep (SSH agent + VSCode Remote-SSH + fonts), bootstrap orchestrator, per-script effects, project clone, infra boot, IDE flow A / B / C, apf-ai-service .NET appendix, troubleshooting. | | `docs/setup/02-wsl-terminal-setup.md` | Renamed from `01-wsl-terminal-setup.md`. No content change. | | `docs/setup/03-dev-web-stack.md` | Renamed from `02-dev-web-stack.md`. No content change. | | `docs/setup/04-angular-nx-monorepo.md` | Renamed from `03-angular-nx-monorepo.md`. No content change. | | `docs/setup/README.md` | **New.** Index of the `docs/setup/` folder. | | `docs/setup/scripts/lib.sh` | **New.** Shared helpers — colour-coded log/ok/warn/err/skip, `apt_install` skipping already-installed, `ensure_line` idempotent append, `confirm` prompt. | | `docs/setup/scripts/bootstrap.sh` | **New.** Orchestrator running scripts 10..80 in order with confirmation prompts. | | `docs/setup/scripts/10-base-packages.sh` | **New.** apt update + base packages (curl, wget, git, build-essential, …). | | `docs/setup/scripts/20-zsh.sh` | **New.** zsh + Oh My Zsh (RUNZSH=no, no shell hijack) + Powerlevel10k + `zsh-autosuggestions` + `zsh-syntax-highlighting`. Patches `~/.zshrc` (theme, plugins, fzf hook). | | `docs/setup/scripts/30-cli-tools.sh` | **New.** `bat eza fd-find ripgrep fzf zoxide ncdu keychain` + `jq yq httpie make tree htop tmux direnv dnsutils unzip rsync`. Symlinks Debian-renamed binaries (`batcat`→`bat`, `fdfind`→`fd`) into `~/.local/bin` so notes/aliases.zsh works as-is. | | `docs/setup/scripts/40-node.sh` | **New.** nvm v0.40.1 + Node from `.nvmrc` (currently 24) + corepack enable + pnpm warmed from `package.json#packageManager`. | | `docs/setup/scripts/50-docker.sh` | **New.** Docker CE + compose plugin from docker.com apt repo, user added to `docker` group, `docker.service` enabled at boot. Pinned GPG key + repo line for Debian Trixie. | | `docs/setup/scripts/60-tuning.sh` | **New.** `fs.inotify.max_user_watches=524288` (Vite/Nx watch ceiling), optional 4 GB swapfile, optional hostname rename (only prompts on generic hostnames like `debian13`). | | `docs/setup/scripts/70-hardening.sh` | **New.** Best-effort UFW (`allow OpenSSH` only) + unattended-upgrades on security channel + fail2ban + sshd drop-in (`PermitRootLogin no`, `PasswordAuthentication no`, `AllowAgentForwarding yes`). Probes before applying, validates `sshd -t` before reloading, skips cleanly if infra already locked the box down. | | `docs/setup/scripts/80-dotfiles.sh` | **New.** Symlinks `notes/aliases.zsh` → `~/.oh-my-zsh/custom/aliases.zsh` (backs up an existing target). Copies `notes/gitconfig.txt` to `~/.gitconfig`, prompts for identity, applies via `git config --global user.name/email`. | | `docs/setup/systemd/apf-portal-infra@.service` | **New.** Template systemd unit auto-starting `./infra/local/dev.sh up` at boot. Install: `enable apf-portal-infra@$USER.service`. | | `.devcontainer/devcontainer.json` | **New.** VSCode Dev Container spec on `mcr.microsoft.com/devcontainers/typescript-node:1-24-bookworm`. `docker-outside-of-docker` feature + `--network=apf-portal-dev` so DNS to `postgres`/`redis`/`otel-collector` works. `initializeCommand` fails fast if `dev.sh up` hasn't been run yet. Forwarded ports labelled. Six dev extensions pre-installed. | | `.devcontainer/post-create.sh` | **New.** `corepack enable && pnpm install --frozen-lockfile`. | | `CLAUDE.md` | "Environment conventions" rewritten: documents the two envs (`local` / `development`) + hybrid sub-mode + the two IDE flows (Remote-SSH / Devcontainer), points at the new VM setup doc and the legacy WSL doc. | ## Key choices - **Idempotent scripts, individually runnable.** Each script probes before doing anything (apt package already installed? plugin already cloned? UFW already active? sshd drop-in already present?). Bootstrap is the orchestrator; each script also runs standalone. Re-running after a partial setup is safe and reports `↪ skip` for the no-op cases. - **No private key on the VM — SSH agent forwarding instead.** `~/.ssh/config` on the workstation carries `ForwardAgent yes`; the VM never holds long-lived secrets. Same future pattern for GPG signing (covered in §8.5 as appendix). `keychain` is installed by `30-cli-tools.sh` as a fallback for scenarios where agent forwarding is not available (CI runners, scripts). - **Hardening is "best-effort, probe-first".** Some infra teams ship pre-hardened VMs; this script doesn't fight that. UFW already active? Print rules and skip. unattended-upgrades already on? Skip. SSH already locked down? Skip. Each section validates before reloading so a misconfig can't take SSH offline. - **Devcontainer assumes infra-on-host.** The container runs on the VM but talks to postgres / redis / otel **on the same VM's host docker daemon** through the shared `apf-portal-dev` Compose network. `initializeCommand` fails fast with a clear message if `./infra/local/dev.sh up` hasn't been run yet — better than puzzling `ECONNREFUSED` errors at runtime. - **`60-tuning.sh` raises inotify to 524288.** Default Debian limit is 8K; Vite/Nx in this monorepo blow past that. The setting is persisted in `/etc/sysctl.d/99-apf-portal.conf` so it survives reboot. - **Hybrid mode (workstation IDE + VM infra) is a documented sub-mode.** SSH `LocalForward` directives on 5432/6379/4317/4318 expose the VM's infra services as `localhost:*` on the workstation. Latency cost: 5-15 ms per query, fine for daily work; for long-running flows, wrap in `tmux` or use `autossh`. ## Notes for the reviewer - **Renaming the existing setup docs (01→02, 02→03, 03→04) is the only "destructive" change.** `git log --follow` still works because of `git mv`. Diff shows up as renames, not delete-and-add. - **The brief asked for `bat eza fd-find ripgrep fzf zoxide docker keychain ncdu git`.** `git` is installed by `10-base-packages.sh` (every other script needs it). Everything else lives in `30-cli-tools.sh` + `50-docker.sh`. The "fullstack-dev extras" (`jq yq httpie make tree htop tmux direnv dnsutils unzip rsync`) are additions I proposed in the lock-in question and that you greenlit (full scope) — easy to trim if any of them turn out to be unwanted. - **Both Node toolchains in parallel** — nvm on the VM (via `40-node.sh`) **and** devcontainer in the repo. Devs can use either; both read the same `.nvmrc` + `packageManager` pin so the version stays consistent. - **The systemd unit is a TEMPLATE** (`apf-portal-infra@.service`) — install once, enable per-user (`enable apf-portal-infra@$USER.service`). This is the right shape for a shared VM with multiple devs eventually, even if today only one user uses it. - **No PR-body Co-Authored-By trailer, no Generated-with-Claude footer**, per the project rule. ## Test plan Manual (no automated test exists for this kind of setup work): - [ ] On a fresh Debian 13 VM: `git clone …`, `./docs/setup/scripts/bootstrap.sh`, answer prompts, end up with zsh + Powerlevel10k + all the requested CLI tools + Node 24 + pnpm 10.33.4 + Docker on PATH. - [ ] `./infra/local/dev.sh up` boots successfully against the VM's local docker daemon. - [ ] `pnpm install` + `pnpm exec nx run-many -t lint test --parallel=3` passes on the VM. - [ ] VSCode Remote-SSH from a workstation: connect, open `~/Works/apf_portal`, run `pnpm exec nx serve portal-bff`, confirm reaches `postgres:5432`. - [ ] VSCode Dev Containers from the same workstation: `Reopen in Container`, image builds, `postCreateCommand` runs `pnpm install`, dev server reaches postgres through the `apf-portal-dev` network. - [ ] Hybrid mode: SSH tunnel from workstation, `pnpm exec nx serve portal-bff` locally, confirm postgres reachable via `localhost:5432`. - [x] `pnpm exec prettier --check` clean on the touched markdown files. - [x] Scripts pass `bash -n` (syntax check) — verified during writing. ## What's next - Validate by walking through this doc on the actual VM `10.100.201.21`. Any friction surfaced becomes a follow-up PR (`docs(setup): ...`). - Once the dev VM is operational, return to **ADR-0027 Implementation PR 1** (Region / Delegation / Structure Prisma schema + inline reference-data migration) — paused since the start of this PR. - Set up the **private dotfiles repo** (`apf/dotfiles`) as a small follow-up, then teach `80-dotfiles.sh` to prefer the dotfiles repo over `notes/`. - When migrating to GitLab: PR pair — (a) `git remote set-url` doc updates here, (b) `infra/gitlab-runners/` replacing `infra/ci-runners.compose.yml`. - Future: deploy the **shared preview infra** on `vm-gitlab` (10.100.201.10) — CI-driven, separate PR. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #220
25 KiB
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: Claudetrailer or a🤖 Generated with Claude Codefooter 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
.mdfiles 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.mdwith 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 indocs/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
appspreset, managed by pnpm — see ADR-0002. - Naming: workspace
apf-portal; appsportal-shell(end-user SPA),portal-admin(admin SPA, skeleton in place — see ADR-0020), andportal-bff(backend); libsfeature-<name>andshared-<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
amrclaim 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-pinofor structured JSON logs, OpenTelemetry SDK + auto-instrumentations for traces, W3C Trace Context propagation across SPA → BFF → DB → Redis,nestjs-clsfor 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.eventsschema in the same Postgres instance, append-only by Postgres role grants (audit_writerINSERT,audit_readerSELECT,audit_archiverDELETE older than retention; noUPDATE/TRUNCATEto anyone); 365-day retention default; cross-referenced with app logs viatrace_idandactor_id_hash(same salt); blocking writes (no audit ⇒ no action) — see ADR-0013. - Downstream API access: unified
DownstreamApiClient(@nestjs/axios+cockatiel), per-serviceDownstreamApiConfig; 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 + signedX-User-AssertionJWT (BFF JWKS at/.well-known/jwks.json); per-call audience pre-check; noaxios/fetchoutsidesrc/downstream/— see ADR-0014. - CI/CD: Gitea Actions (level-2 implementation; will be superseded by a GitLab migration ADR within 6-18 months). Trunk-based with squash-merge, branch protection on
main, all CI gates blocking. Thin YAML — orchestration logic lives inpackage.jsonscripts (ci:check,ci:scan,ci:commits) and Nx targets, runnable locally. Gates: format / lint / type-check / test / build / audit / secret-scan / commit-lint, plusa11y(per ADR-0016) and futureperf. Self-hostedact_runneron-prem. 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 recommended, revisited at GitLab migration — see ADR-0015. - 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/playwrighte2e (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/accessibilityand/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 bundlebudgets(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+fileReplacementsat build time (no runtime config-fetch). BFF readsprocess.envdirectly with small per-key boot-time validators (no@nestjs/configoverhead at this scale). The audit log uses a separateAUDIT_DATABASE_URLconnection pool in production (audit_writer-only login, defense in depth) and falls back to the shared pool +SET LOCAL ROLEin dev — see ADR-0018. - Internationalisation:
@angular/localizein build-time mode, two locales (frdefault served at/,en), source locale = English (project English-only rule). Path-based URLs always prefixed (/fr/...,/en/...);/smart-redirects via cookie →Accept-Language→fr. 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_localecookie and hard-refreshes — see ADR-0019. - Admin application (
portal-admin): dedicated Angular SPA alongsideportal-shell, sharing the sameportal-bffvia/api/admin/*routes guarded by an EntraPortal.Adminrole +@RequireMfa({ freshness: 600 })at entry. Distinct origin / cookie / session fromportal-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 forportal-shell); same a11y + dark-mode baseline. Shared UI primitives (Icon,LayoutStateService, brand tokens) graduate tolibs/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/**/*.mdrendered as a separate static site via VitePress (Vite-based, Node-only toolchain, Markdown-first). Mermaid diagrams viavitepress-plugin-mermaid. Deployed on its own hostname behind the shared reverse-proxy; CI hook ondocs/changes rebuilds + publishes. Decoupled from the apps — content lives indocs/, no in-app Markdown viewer — see ADR-0022. - Charts + dashboards:
D3 + Observable Plotwrapped inlibs/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-motiongate). 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-servicerepo (ASP.NET Core, Microsoft Agent Framework) consumed via native gRPC HTTP/2 only — proto contract vendored underapps/portal-bff/src/grpc/proto/apf-ai/withts-protocodegen committed alongside. BFF dials with@grpc/grpc-js(h2c in dev, h2 + TLS in prod), bridgesChatService.Chattotext/event-streamfor the SPA, exposesRagService.SearchandModelsService.ListModelsas plain JSON endpoints. Identity travels as an unsignedPrincipal(subject, roles, attributes) in the proto body for the POC, hashed via the audit module'sHashUserIdServiceso portal and AI service audit trails join on the sameactor_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 → curatedapf-role-*slug catalogue, 24 entries v1), scopes (portal-sideuser_scopestable, future Pléiades feed; kinds =self / etablissement:<structure-code> / delegation:<dept> / region:<insee> / siege / unrestricted, see ADR-0027 for theStructure.codesemantics). Composed at sign-in into a session-residentPrincipal; portal guards consume the structured shape, a deterministicPrincipalProjectorflattens it to the AI-serviceroles[]contract. Replaces stargate's linear hierarchy. Catalogues are closed-set, drift gated by CI — see ADR-0025. - Portal-side identity model:
Persongolden record (stable identity, can exist without a portal account — workforce pre-provisioning, dossier bénéficiaires, alumni) +Useroverlay (one-to-zero-or-one with Person, lazy-created on first OIDC callback in v1; carries portal-only state likelastSignInAt).UserScopebacks the ADR-0025 scope axis with opaquevaluestrings referencing ADR-0027'sStructure.code/Delegation.code/Region.code— no FK at the DB level so historical rows survive structure decommissioning; admin-UI write path validates. v1 dedup usesentraOidonly;Person.emailis an indexed attribute, not a unique key, because two distinct humans genuinely share emails (shared aliases, genericinfo@, upstream-feed errors). Facets (Salarié / Élu / Adhérent / Bénéficiaire) + Pléiades / Acteurs+ sync + operator-confirmed Person-merge flow deferred to ADR-0028 — see ADR-0026. - Portal-side organisational hierarchy:
Region(INSEE 2-digit) →Delegation(department 2–3-char) →Structurewithkinddiscriminator (medico_social/antenne/dispositif/entreprise_adaptee/mouvement/administratif/siege, aligned with cascade'sStructure.type).Structure.codeis 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/codePaieare 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, plusPole/Service/ arbitrary nesting / per-source enrichment, land additively with ADR-0028 — see ADR-0027. - Runtime: Node.js latest LTS major.
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 → 0027 are accepted and cover the structural, security, observability, quality, i18n, admin-app, docs-site, charts, AI-relay, authorization, and portal-side identity + organisational hierarchy choices. ADR-0028 (cascade / Pléiades / Acteurs+ syncs + facet schemas) is the next proposed addition; until it ships, ADR-0026 + ADR-0027 implementation PRs together unlock the ADR-0025 stubs (Principal.user.{id, personId} placeholders, StubScopeResolver's unrestricted blanket return). Shipped on main:
- Phase-1 foundation — Nx workspace, Angular
portal-shell, NestJSportal-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.eventsappend-only schema with role-based grants, helmet + env-driven CORS allowlist + rate limiting + structured error envelope (see ADR-0021). - Phase-3a admin app —
portal-adminSPA 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) +AiBridgeControllerexposingPOST /api/ai/chat(SSE),GET /api/ai/rag/search,GET /api/ai/models(see ADR-0024). Chatbot widget live inportal-shellatapps/portal-shell/src/app/features/chatbot/. - Docs static site (ADR-0022) — VitePress + Mermaid renderer at
docs/.vitepress/, dedicateddocs-site.ymlworkflow that rebuilds + publishes on everydocs/**change. - Charts lib + audit-page dashboards (ADR-0023) —
libs/shared/charts/withBarChart,DonutChart,StackedBarChart(D3 + Observable Plot, headless / a11y-baked-in), integrated into the/admin/auditpage 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),Principalshape, pure matchers, andEntraGroupToRoleResolver. BFF-sidePrincipalBuildercomposing the three axes at sign-in from Entraroles+groupsclaims + a stubScopeResolver.@RequirePrivilege/@RequireRole/@RequireScoperoute decorators + guards with the ADR-0021 structured-error envelope on denial;AdminRoleGuardmigrated to readprincipal.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.@RequireScopePrisma-backed resolver + first consumer surface —StubScopeResolverreturnsunrestrictedfor everyone in v1 per ADR-0025 §331. Implementation lands across ADR-0026 (accepted:Person+User+UserScope) and ADR-0027 (accepted:Region/Delegation/Structurewithkinddiscriminator + nullable FINESS / SIRET). Sequencing: ADR-0026 PR 1 + ADR-0027 PR 1 ship schema in parallel; ADR-0026 PR 2 then lands thePrismaScopeResolver+ admin scope-seeding UI + test-tenant seed (which references ADR-0027'sStructure.codevalues). The follow-up ADR-0028 covers Pléiades / Acteurs+ / cascade syncs + facet schemas.- Proto-drift CI gate for the AI relay (ADR-0024) — asserts the vendored
apf-ai-serviceproto 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) anddevelopment(Debian 13 VM at10.100.201.21— the default for new devs, replaces WSL). Ahybridsub-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.jsonshipped, Node + pnpm pinned in the image, mounts the docker socket so the host'sapf-portal-devCompose network is reachable from inside). - Never install Angular globally. Use
pnpm dlxfor one-off CLI invocations and project-localpnpm 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-workspaceskill 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
--helpfirst when unsure
Scaffolding & Generators
- For scaffolding tasks (creating apps, libs, project structure, setup), ALWAYS invoke the
nx-generateskill 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-generateskill handles generator discovery internally - don't call nx_docs just to look up generator syntax