# 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/](docs/), indexed by [docs/README.md](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/](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](docs/decisions/template.md).
- Location: flat folder [docs/decisions/](docs/decisions/), indexed by [docs/decisions/README.md](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](docs/decisions/0002-adopt-nx-monorepo-apps-preset.md).
- **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](docs/decisions/0003-workspace-and-app-naming-convention.md).
- **Frontend (`portal-shell`):** Angular at the latest LTS major — standalone APIs, zoneless change detection, Signals, **CSR only (no SSR)**, Vitest, SCSS — see [ADR-0004](docs/decisions/0004-frontend-stack-angular-csr-zoneless-signals.md).
- **Backend (`portal-bff`):** NestJS at the latest stable major, mounted on the Express adapter (Fastify adapter swappable later) — see [ADR-0005](docs/decisions/0005-backend-stack-nestjs.md).
- **Persistence:** PostgreSQL (latest stable major) via Prisma — see [ADR-0006](docs/decisions/0006-persistence-postgresql-prisma.md).
- **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](docs/decisions/0010-session-management-redis.md).
- **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](docs/decisions/0011-mfa-enforcement-entra-conditional-access.md).
- **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](docs/decisions/0008-identity-model-entra-workforce-dual-audience.md).
- **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](docs/decisions/0009-auth-flow-oidc-pkce-msal-node.md).
- **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](docs/decisions/0012-observability-pino-opentelemetry.md).
- **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](docs/decisions/0013-audit-trail-separated-postgres-append-only.md).
- **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](docs/decisions/0014-downstream-api-access-obo-pattern.md).
- **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 in `package.json` scripts (`ci:check`, `ci:scan`, `ci:commits`) and Nx targets, runnable locally. Gates: format / lint / type-check / test / build / audit / secret-scan / commit-lint, plus `a11y` (per ADR-0016) and future `perf`. Self-hosted `act_runner` on-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](docs/decisions/0015-cicd-gitea-actions.md).
- **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](docs/decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md).
- **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](docs/decisions/0017-performance-budgets-lighthouse-ci.md).
- **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](docs/decisions/0018-environment-configuration-strategy.md).
- **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-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_locale` cookie and hard-refreshes — see [ADR-0019](docs/decisions/0019-internationalisation-angular-localize.md).
- **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](docs/decisions/0020-portal-admin-app.md).
- **Local quality gates:** Husky + lint-staged + commitlint with Conventional Commits — see [ADR-0007](docs/decisions/0007-pre-commit-hooks-and-conventional-commits.md).
- **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](docs/decisions/0022-docs-site-vitepress.md).
- **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 four lib roots (`libs/feature/`, `libs/shared/state`, `libs/shared/tokens`, `libs/shared/ui`, `libs/shared/util`) are in place; CI runs `format:check / lint / test / build` on every PR.

ADRs 0001 → 0022 are accepted and cover the structural, security, observability, quality, i18n, admin-app, and docs-site choices. **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](docs/decisions/0021-phase-2-security-baseline.md)).
- **Phase-3a admin app skeleton** — `portal-admin` SPA exists with brand tokens and routing; business modules (CMS, menu management, user list, audit log viewer) not yet implemented.

**Still on the roadmap:**

- `DownstreamApiClient` + OBO ([ADR-0014](docs/decisions/0014-downstream-api-access-obo-pattern.md)) — no v1 consumer yet; will land when the first business route needs an Entra-protected API.
- `@RequireMfa()` / `@RequireAdmin()` guards ([ADR-0011](docs/decisions/0011-mfa-enforcement-entra-conditional-access.md), [ADR-0020](docs/decisions/0020-portal-admin-app.md)) — designed-in, awaiting first consumer route.
- **Docs static-site implementation** ([ADR-0022](docs/decisions/0022-docs-site-vitepress.md)) — ADR accepted, chantier (VitePress install + `.vitepress/config.ts` + `docs-site.yml` workflow) lands next.
- **Strategic security baseline ADR** — separate from the implementation-level [ADR-0021](docs/decisions/0021-phase-2-security-baseline.md). 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`:

```bash
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:

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

Workspace-wide:

```bash
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

- **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.
- **Work inside the WSL filesystem** (`~/dev/...`), never under `/mnt/c` — the latter has severe I/O penalties that break Nx caching and dev-server reload times.
- **pnpm is mandatory** (activated via `corepack enable`); do not introduce npm or yarn lockfiles.
- Prettier config target: `singleQuote: true`, `semi: true`, `printWidth: 100`.

<!-- nx configuration start-->
<!-- Leave the start & end comments to automatically receive updates. -->

## 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

<!-- nx configuration end-->
