# Development guide This document is the day-to-day reference for working on `apf_portal`. It covers the repo layout, the prerequisites, the initial setup from a fresh clone, and the commands you'll run during a typical development cycle. It is meant to grow — add sections as the team's workflow does. For decision rationale, see the [ADRs](decisions/). For onboarding the local environment (terminal, Node, pnpm), see [setup/](setup/). --- ## 1. Repo layout ``` apf_portal/ ├── .gitea/workflows/ # CI pipelines (ADR-0015) │ ├── ci.yml # per-PR + push to main: check / scan / commits / perf / a11y │ └── security-scheduled.yml # weekly full-tree scan + prod Lighthouse ├── .github/ # Nx AI-tooling skills, prompts, agents (Nx-managed) ├── .husky/ # local git hooks (ADR-0007) │ ├── pre-commit # → pnpm exec lint-staged │ └── commit-msg # → pnpm exec commitlint ├── apps/ │ ├── portal-shell/ # Angular 21 SPA (zoneless, standalone, Signals, Vitest, SCSS) │ │ ├── public/ # static assets │ │ ├── src/ # entry, app config, routes, styles │ │ ├── .postcssrc.json # Tailwind PostCSS plugin (required by @angular/build esbuild — postcss.config.js is ignored) │ │ └── project.json # Nx project config (build, serve, test, lint targets) │ ├── portal-shell-e2e/ # Playwright e2e for portal-shell │ ├── portal-bff/ # NestJS 11 BFF (Express adapter, ValidationPipe, Jest) │ │ ├── src/ # main, app module, controllers, services │ │ ├── prisma/schema.prisma # Prisma 7 schema (postgresql) │ │ ├── prisma.config.ts # Prisma 7 TS config (loads DATABASE_URL from .env) │ │ ├── .env.example # env-vars catalog (committed); .env stays gitignored │ │ └── project.json │ └── portal-bff-e2e/ # Jest e2e for portal-bff ├── libs/ │ ├── feature// # vertical feature libs (e.g. feature-auth) │ └── shared// # cross-cutting libs (tokens, ui, util) ├── docs/ │ ├── README.md # doc index │ ├── decisions/ # ADRs (MADR 4.0.0) │ ├── setup/ # local-environment onboarding (Zsh, pnpm, Nx workspace) │ └── development.md # this file ├── notes/ # personal scratchpad (gitignored) ├── CLAUDE.md # project rules + architecture summary ├── commitlint.config.cjs # Conventional Commits config ├── eslint.config.mjs # workspace ESLint with module boundaries ├── lighthouserc.js # Lighthouse CI thresholds (ADR-0017) ├── nx.json # Nx workspace config ├── package.json # workspace deps + scripts ├── pnpm-workspace.yaml # apps/* + libs/** ├── tsconfig.base.json # shared TS strict config └── vitest.workspace.ts # Vitest workspace projects ``` The conventions that govern this layout are recorded in: - [ADR-0002](decisions/0002-adopt-nx-monorepo-apps-preset.md) — Nx workspace shape - [ADR-0003](decisions/0003-workspace-and-app-naming-convention.md) — naming convention (`portal-shell`, `portal-bff`, `feature-`, `shared-`) - [ADR-0007](decisions/0007-pre-commit-hooks-and-conventional-commits.md) — local hooks - [ADR-0015](decisions/0015-cicd-gitea-actions.md) — CI/CD shape --- ## 2. Prerequisites A working dev machine for `apf_portal` needs: | Tool | Why | How | | -------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | **WSL 2 + Debian** (Windows) or Linux/macOS native | All commands assume a POSIX shell | see [setup/01](setup/01-wsl-terminal-setup.md) | | **Node.js 24** (latest LTS) | Runtime, pinned in `.nvmrc` | `nvm install 24 && nvm use` (see [setup/02](setup/02-dev-web-stack.md)) | | **pnpm 10+** | Mandatory package manager (no npm/yarn lockfile) | `corepack enable && corepack prepare pnpm@latest --activate` | | **Git ≥ 2.40** | Husky 9 + signed commits eventually | usually default | | **mkcert** | Local HTTPS for cookie-prefix `__Host-` (ADR-0009) | `apt install mkcert` then `mkcert -install` | | **Trivy** _(optional, locally)_ | Dep vuln scan when running `ci:scan` locally; CI uses an action | `apt install trivy` or [trivy install docs](https://trivy.dev/) | | **gitleaks** _(optional, locally)_ | Same pattern; CI uses an action | `apt install gitleaks` or [gitleaks install docs](https://github.com/gitleaks/gitleaks#installing) | | **Docker** | For the local-dev stack (Postgres + Redis + OTel + viewers) per [`infra/local/`](../infra/local/) | Docker Desktop on Windows, Docker Engine on Linux | | **`psql`** (postgresql-client) | Inspect the local Postgres from the host (audit roles, schemas, queries). Versions `>= 16` are fine | `apt install postgresql-client` | | **`redis-cli`** (redis-tools) | Inspect the local Redis from the host (sessions, OBO cache when they land) | `apt install redis-tools` | Work inside the WSL filesystem (`~/Works/...`), never `/mnt/c/...` — the latter has severe I/O penalties that break Nx caching. --- ## 3. Initial setup from a fresh clone ```bash git clone gitea@git.unespace.com:julien/apf_portal.git cd apf_portal # Install deps (also runs `husky` to wire git hooks) pnpm install # Generate the Prisma client (until you set up the DB it errors on # missing DATABASE_URL — that's expected; the generation only reads # the schema, not the DB). cd apps/portal-bff && pnpm exec prisma generate && cd ../.. # Sanity check pnpm nx run-many -t lint test build ``` For the BFF to actually run end-to-end, you'll also need the local infrastructure stack — Postgres, Redis, OpenTelemetry Collector — provisioned via Docker Compose. A thin wrapper script ([`infra/local/dev.sh`](../infra/local/dev.sh)) hides Compose-profile quirks and gives ergonomic verbs: ```bash # 1. Configure infra secrets (copy template, edit, do not commit). cp infra/local/.env.example infra/local/.env $EDITOR infra/local/.env # Set strong dev values for POSTGRES_PASSWORD and REDIS_PASSWORD. # 2. Bring up the core stack (postgres + redis + otel-collector). ./infra/local/dev.sh up # 3. (Optional) Activate viewers when debugging: ./infra/local/dev.sh up dbtools # adds pgweb (http://localhost:8081) ./infra/local/dev.sh up observability # adds Jaeger UI (http://localhost:16686) ./infra/local/dev.sh up all # core + every profile # 4. App-side env from .env.example, then fill in DATABASE_URL pointing # to the compose-managed Postgres (matches the values you set in # infra/local/.env). cp apps/portal-bff/.env.example apps/portal-bff/.env ``` `./infra/local/dev.sh help` lists the rest of the verbs (`down`, `status`, `logs`, `stop`, `restart`, `exec`). Full reference — service inventory, port table, persistence, bootstrap re-run procedure — lives in [`infra/README.md`](../infra/README.md) → "Local-dev stack". --- ## 4. Daily commands ### Run the apps ```bash pnpm nx serve portal-shell # http://localhost:4200 (Angular dev server) pnpm nx serve portal-bff # http://localhost:3000/api (NestJS) ``` Both can run in parallel in two terminals; the SPA proxies API calls to the BFF in dev. ### Test ```bash pnpm nx test portal-shell # Vitest (single run; --configuration=watch for watch mode) pnpm nx test portal-bff # Jest pnpm nx run-many -t test # all projects pnpm nx affected -t test # only projects affected since main ``` Run a single Vitest file: ```bash pnpm nx test portal-shell --testFile=src/app/app.spec.ts ``` ### Lint, type-check, format ```bash pnpm nx lint portal-shell # one project pnpm nx run-many -t lint # all projects pnpm nx affected -t lint # affected only pnpm nx affected -t lint --fix # auto-fix where possible pnpm nx affected -t type-check # explicit type-check (independent of test/build) pnpm nx format:write # apply Prettier pnpm nx format:check # CI-style verification ``` ### Build ```bash pnpm nx build portal-shell # development build pnpm nx build portal-shell --configuration=production # production build pnpm nx run-many -t build pnpm nx affected -t build ``` ### Generate ```bash # Component in portal-shell pnpm nx g @nx/angular:component --project=portal-shell --standalone # Service / controller / module in portal-bff pnpm nx g @nx/nest:service --project=portal-bff pnpm nx g @nx/nest:controller --project=portal-bff # New shared lib (TS-only, consumable by both apps) pnpm nx g @nx/js:library --name=shared- --directory=libs/shared/ \ --bundler=tsc --unitTestRunner=vitest \ --tags="scope:shared,type:shared" --no-interactive # New Angular feature lib (front-only) pnpm nx g @nx/angular:library --name=feature- --directory=libs/feature/ \ --standalone=true --unitTestRunner=vitest-analog \ --tags="scope:portal-shell,type:feature" --no-interactive ``` > Sweep generated files for `process.env.X` (dot notation) → `process.env['X']` (bracket notation), required by the strict-TS option `noPropertyAccessFromIndexSignature: true`. The Nx generators don't emit bracket form. ### Prisma ```bash # Regenerate the typed client after schema changes cd apps/portal-bff && pnpm exec prisma generate && cd ../.. # Create and apply a migration in dev cd apps/portal-bff && pnpm exec prisma migrate dev --name && cd ../.. # Deploy migrations in prod (run by deploy pipeline, not locally) cd apps/portal-bff && pnpm exec prisma migrate deploy && cd ../.. # Inspect the dev DB cd apps/portal-bff && pnpm exec prisma studio && cd ../.. ``` ### CI scripts (runnable locally) Mirror what the CI does on every PR: ```bash pnpm ci:check # nx affected -t format:check lint test build pnpm ci:audit # pnpm audit --audit-level=moderate pnpm ci:commits # commitlint on the PR commit range (uses $COMMIT_LINT_FROM, defaults to origin/main) pnpm ci:perf # production build + Lighthouse CI against the static-served bundle ``` `ci:scan` (Trivy + gitleaks) is currently invoked from CI YAML rather than as a `pnpm` script — those tools are Go binaries without clean npm wrappers. Run them locally if you've installed the binaries. --- ## 5. Observability dev-loop The BFF and the SPA are both wired with OpenTelemetry tracing and structured Pino logging (per [ADR-0012](decisions/0012-observability-pino-opentelemetry.md)). This section is the practical guide to using them while debugging — finding a trace, reading the logs that go with it, untangling a slow request. ### Bring up the observability stack Traces land in the Jaeger UI bundled in `infra/local/dev.compose.yml` under the `observability` profile. The OpenTelemetry Collector runs in the core profile; without Jaeger active, traces still go to the Collector but nothing visualises them — they appear as warnings in `dev.sh logs otel-collector` and are dropped after the buffer fills. ```bash ./infra/local/dev.sh up observability # core stack + Jaeger pnpm nx serve portal-bff # http://localhost:3000 pnpm nx serve portal-shell # http://localhost:4200 ``` | Endpoint | Purpose | | ---------------------- | --------------------------------------------------------------------------------------- | | http://localhost:16686 | Jaeger UI — search and explore traces | | http://localhost:4318 | OTLP/HTTP receiver on the Collector — both the BFF and the SPA push spans here directly | | (BFF stdout) | Pino logs — JSON in prod, `pino-pretty` colourised one-liners in dev | | (Browser DevTools) | SPA spans — visible in Network as POSTs to `/v1/traces` | ### Reading a trace in Jaeger 1. Open http://localhost:16686. 2. Pick the **Service** dropdown — `portal-bff` for backend traces, `portal-shell` for SPA traces. After a fetch SPA → BFF, the trace shows up under whichever service initiated the root span (`portal-shell` if the user clicked, `portal-bff` if it was a direct curl). 3. **Find Traces** lists recent traces; click one to drill in. Each row in the timeline view is a span. Spans are nested by parent-child: `document_load` → `fetch` → `HTTP GET /api/...` → `prisma.findMany` → `pg.SELECT`. 4. Click a span to see its attributes, events, and resource. Useful keys to look at: - `http.method`, `http.status_code`, `http.url` — request shape - `db.statement`, `db.system` — actual SQL run by Prisma - `service.name` / `service.version` — which app emitted the span - `trace_id` (top of the trace) — what to grep in the logs ### Correlating a trace with the BFF logs Every Pino log line emitted by the BFF during a traced request carries a `trace_id` and `span_id` field, injected automatically by `@opentelemetry/instrumentation-pino`. Concretely: ```bash # 1. Pick a trace_id in the Jaeger URL bar (the long hex string after /trace/). # 2. Grep the BFF stdout — capture from the serve-portal-bff terminal, # or pipe the dev server output to a file. pnpm nx serve portal-bff 2>&1 | tee /tmp/portal-bff.log grep '"trace_id":""' /tmp/portal-bff.log ``` In production the same field exists on every JSON log line; the log aggregator's UI can filter by it. Same trace, same `trace_id`, all the lines. The `request_id` field (per-request UUID stored in `nestjs-cls`) is also on every line — useful for filtering when you don't have a trace handy yet. ### Reading SPA spans from the browser The SPA pushes its spans straight to the Collector via OTLP/HTTP. Two ways to inspect what gets sent: - **DevTools → Network tab**: filter on `localhost:4318/v1/traces`. Each entry is a batch of spans encoded as JSON. Useful for diagnosing "why doesn't my span show up" — if the POST never happens, the span never left. - **Jaeger UI → service `portal-shell`**: same data, after the Collector forwards it. End-to-end view. The auto-instrumentations in v1 cover: - `document_load` — one span per page load, with sub-spans for DNS, TCP, request/response, DOM - `fetch` — one span per outgoing fetch, with W3C `traceparent` propagated to the BFF (so the BFF picks up the same trace id and emits child spans) - `user_interaction` — clicks, keypresses, submits ### Common gotchas - **No spans appear in Jaeger but the Collector logs receive them**. The `observability` profile probably wasn't active when you brought the stack up. `./infra/local/dev.sh status` should list `apf-portal-jaeger`. If not, `./infra/local/dev.sh up observability`. - **CORS errors in DevTools when the SPA POSTs to `/v1/traces`**. The Collector's `cors.allowed_origins` in `infra/local/otel-collector.yaml` must include the SPA dev origin (`http://localhost:4200`). It does by default; if you're serving on a non-default port, add it there and re-up the Collector. - **`document_load` span appears but `fetch` spans do not propagate to the BFF as children**. Two failure modes: - The BFF's CORS allowlist for `traceparent` / `tracestate` is missing — see `apps/portal-bff/src/main.ts` `enableCors`. - The fetch URL doesn't match `propagateTraceHeaderCorsUrls` in `apps/portal-shell/src/observability/tracing.ts`. The default regex matches `localhost:3000`; non-default BFF ports need to be added. - **Logs come out raw JSON in dev instead of `pino-pretty`**. `NODE_ENV` is set to `production` somewhere upstream (Nx's serve target normally exports `development`). Check `process.env.NODE_ENV` from inside the BFF. - **`nx serve portal-bff` errors with `EADDRINUSE :::3000`**. A previous BFF process didn't shut down cleanly. `pkill -f portal-bff` or `ss -ltnp | grep :3000` + `kill `. ### What's not in v1 A few things ADR-0012 sketches that aren't wired yet, in case you go looking: - CLS keys `session_id`, `user_id_hash`, `audience` — placeholders only; populated by guards/interceptors when ADR-0009 (auth) and ADR-0010 (sessions) land. - Pino `redact` list for PII — wired with the first DTO carrying redactable data. - Custom domain spans (`tracer.startActiveSpan('user.create', …)`) — added per service method as the corresponding feature ships. On the SPA side, custom spans across `await` need explicit `context.with(...)` since the workspace is zoneless. - Production OTLP backend (Tempo / Loki / similar) — phase 3b on-prem ADR. The current setup is dev-only; the Collector's `debug` exporter dumps everything to stdout, useful for local but not what gets pointed at a real backend. --- ## 6. Dependency updates (Renovate) [Renovate](https://docs.renovatebot.com/) runs as a scheduled workflow ([`.gitea/workflows/renovate.yml`](../.gitea/workflows/renovate.yml)) and opens PRs against `main` for dependency updates. Daily at 03:00 UTC, plus on-demand via `workflow_dispatch`. Behaviour is controlled by [`renovate.json`](../renovate.json) at the repo root: groupings (Angular, Nx, NestJS, Prisma, Vitest, TypeScript tooling, ESLint, SWC, Tailwind), Conventional-Commits-compatible commit messages (`chore(deps): …` / `fix(deps): …` for vulnerability fixes), weekly lockfile maintenance, OSV.dev as the vulnerability data source. ### One-time bot onboarding Renovate authenticates as a dedicated bot user. Setup is manual on Gitea — done once per Gitea instance, then the workflow runs unattended. 1. **Create a bot user.** Site Administration → Users → Create User. Suggested name: `apf-portal-bot`. Strong password, mark as **non-admin** (least privilege). 2. **Set the bot's Full Name** in its Gitea profile (User Settings → Profile → Full Name, e.g. `APF Portal Bot`). Without it, Renovate's git commits fail with `empty ident name not allowed`. The `gitAuthor` in `renovate.json` is the explicit override, but keeping the profile value consistent avoids confusion when reading commit history in Gitea's UI. 3. **Add the bot as a collaborator** on this repo with **Write** access (Settings → Collaborators). Without write, Renovate can't push branches. 4. **Generate a PAT for the bot** (`RENOVATE_TOKEN`). Sign in as the bot, then User Settings → Applications → Generate New Token. Scopes needed: read/write `repository`, read/write `issue`, read `user`. Avoid `admin`. 5. **Store the PAT as a repo secret.** Settings → Actions → Secrets → New Secret. Name: `RENOVATE_TOKEN`. Value: the token from step 4. 6. **Generate a zero-scope GitHub.com PAT** (`GITHUBCOM_TOKEN`). On github.com (any account, e.g. yours): Settings → Developer settings → Personal access tokens → Tokens (classic) → Generate new token (classic). **Do not tick any scope** — anonymous-equivalent rights are enough; the token only buys Renovate the higher authenticated rate limit (5 000 req/h vs 60 req/h) for resolving GitHub-hosted Action versions and `containerbase/node-prebuild` binaries used during lockfile maintenance. 7. **Store it as a repo secret named `GITHUBCOM_TOKEN`** (Gitea reserves the `GITHUB_*` secret namespace for the built-in `${{ github.* }}` context, so an underscore between `GITHUB` and `COM` is rejected). 8. **Sign out and forget both tokens locally.** They are now only retrievable via the secret store. To **rotate** either token: regenerate at the matching step, update the secret. The schedule keeps running unattended. ### Triggering manually Repo → Actions → "Renovate" workflow → Run workflow. Useful when you've just changed `renovate.json` and want the next pass to happen immediately rather than wait for the next 03:00 UTC tick. ### Reviewing Renovate PRs - Renovate PRs run a leaner CI pipeline than human PRs — `check`, `scan`, `a11y` only. The `perf` and `commits` gates are skipped (per [ADR-0017](decisions/0017-performance-budgets-lighthouse-ci.md) — Lighthouse signal on a dep bump is essentially zero, commitlint on bot-generated messages is tautological). The full `perf` gate still runs on `push` to `main` post-merge, so regressions are caught seconds after merge rather than before. - **Patch / pin / digest / lockfile-maintenance updates auto-merge** as soon as their CI is green. The bot opens the PR, CI runs, Renovate squashes it into `main` without waiting for a human. They appear briefly in the dashboard's "Open" section while CI runs, then disappear into the merged log. Failures stay open for triage. **Minor and major** updates remain manual. - Don't merge minor PRs until the remaining gates are green. - **Major bumps are gated behind the dependency dashboard.** Renovate does not auto-create PRs for major updates; instead, they appear in the "Renovate Dependency Dashboard" issue with a checkbox. Tick the box only after reading the upstream changelog and confirming the rest of our toolchain (Nx plugins, Angular CLI, NestJS, etc.) supports the new major. This guard exists because `nx affected` sees a deps-only change as not affecting any project — so a major that breaks the build can pass CI silently and only surface days later. Past offenders we caught the hard way: TypeScript 5→6 (deprecated `baseUrl`), ESLint 9→10 (Nx eslint plugin not yet compatible), webpack-cli 5→7 (removed `--node-env` flag). - The "Renovate Dependency Dashboard" issue (auto-created on first run) lists every pending update grouped by status. Use it to triage which PRs to expedite. - For a major bump that introduces breaking changes, **don't reflexively merge**: read the changelog, then either accept the work or close the PR with a "rejected" label. Renovate respects that label and won't keep re-opening the same major. - **Adding or removing** a dependency belongs in a feature PR, not in Renovate's scope. Renovate only updates _versions_ of existing deps. --- ## 7. Conventional commit cycle 1. Branch from `main` with a short slug: ```bash git switch -c feat/portal-shell/auth-login # or fix/..., chore/..., docs/... ``` 2. Commit using **Conventional Commits**. The local `commit-msg` hook (commitlint) rejects anything else. ``` feat(portal-shell): add login flow stub fix(portal-bff): correct env var bracket access chore: bump @nx/* to 22.7.2 docs(decisions): add ADR-0018 for security baseline ``` `pre-commit` runs `lint-staged` → Prettier on staged files. Lint and tests stay in CI. 3. Push and open a PR against `main`. The CI runs: - `check` (lint, type-check, test, build on affected) - `scan` (audit, Trivy, gitleaks) - `commits` (commitlint on the PR commit range) - `perf` (Lighthouse on the production bundle) - `a11y` (axe-core; placeholder until first real screens) All five must be green to merge. PR title must itself be a Conventional Commits message — it becomes the squash-merge subject ([ADR-0015](decisions/0015-cicd-gitea-actions.md)). 4. Squash-merge into `main`. Branch is auto-deleted. Linear history maintained. 5. To cut a release: tag `vX.Y.Z` on `main`. The `release.yml` workflow will pick it up (currently a stub; populated alongside the on-prem deploy ADR). ### PR conventions The squash-merge subject on `main` is the **PR title**, not the individual commits on the feature branch (those collapse into the squash). Two practical consequences: 1. **The PR title must itself be a valid Conventional Commits message.** Same format as a commit message — `(): `, imperative mood, lowercase, no trailing period, target ≤ 70 chars. The CI `commits` job (commitlint on the PR commit range) catches violations. 2. **Individual commits on the feature branch can be exploratory.** The local `commit-msg` hook still validates each commit's format, but the squash makes granular history irrelevant on `main`. Granular history stays available in the PR for review. #### Type vocabulary | Type | When | | ---------- | ----------------------------------------------- | | `feat` | new user-facing feature or capability | | `fix` | bug fix | | `docs` | documentation only (no code) | | `style` | formatting / whitespace (no logic change) | | `refactor` | code change that is neither a fix nor a feature | | `perf` | performance improvement | | `test` | tests added or updated | | `build` | build system, dependencies | | `ci` | CI configuration | | `chore` | maintenance, scaffolding, project metadata | | `revert` | revert a previous commit | #### Scope vocabulary (optional) | Scope | Examples | | ------------- | ----------------------------------------------------------- | | App | `portal-shell`, `portal-bff` | | Lib | `shared-tokens`, `shared-ui`, `shared-util`, `feature-auth` | | Cross-cutting | `decisions` (ADR work), `docs`, `ci`, `deps` | Scope is optional. Omit when the change spans too many areas to scope cleanly (e.g., a workspace-level rename). #### PR body template When a PR is opened against `main`, Gitea pre-populates the body from `.gitea/pull_request_template.md`: - **Summary** — 1–3 bullets describing what changed. - **Motivation** — why, with ADR / issue / incident links. - **Implementation notes** — trade-offs, alternatives considered, follow-ups deferred. - **Verification** — CI gates checked, manual test description, ADR / diagram update flags. - **Related** — ADR-XXXX, related PRs, follow-up issues. The template guides without enforcing — sections can be left blank when irrelevant. The point is to make "what does the reviewer need to know" explicit, not to add ceremony. --- ## 8. Where to look | Question | Doc | | ------------------------------------------- | ---------------------------------------------------------------------------- | | Project rules and the why behind them | [CLAUDE.md](../CLAUDE.md) | | All ADRs (decisions index) | [docs/decisions/README.md](decisions/README.md) | | Initial environment setup (Zsh, Node, pnpm) | [docs/setup/](setup/) | | RSSI briefing for ASVS / HDS / etc. | `notes/asvs-level-decision-briefing-rssi.md` (gitignored, personal) | | The dev-team rationale for the UI stack | `notes/argumentaire-stack-ui-spartan-cdk-tailwind.md` (gitignored, personal) | --- ## 9. Sections to come — roadmap by phase This doc starts as a phase-1 + cross-cutting reference. As features for later phases land, the corresponding sections below are filled in directly. Each entry is mapped to the ADR / implementation work that unlocks it, so a contributor can see when each section becomes real and what triggers it. When a section grows beyond a short subsection, it is extracted to its own file under `docs/development/`. Per the documentation convention (see [README.md](README.md)), we group into a folder once we have at least three related files; this doc is then re-organised into an index pointing at the extracted files. Until then, all sections live here. | Future section | Phase | Triggered by | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Auth dev-loop** — Microsoft 365 Developer tenant configuration, MSAL Node connection, OIDC code-flow walkthrough, switching between dev and prod-like tenants. | 2 | Auth flow code lands ([ADR-0009](decisions/0009-auth-flow-oidc-pkce-msal-node.md)) once the dev tenant is provisioned by IT. | | **Session inspection** — reading the Redis session store in dev, decrypting the AES-GCM `tokens` blob with the dev key, force-logout patterns. | 2 | Sessions module lands ([ADR-0010](decisions/0010-session-management-redis.md)). | | **MFA step-up debugging** — triggering claims-challenge flows, verifying `mfaVerifiedAt` freshness, testing the SPA HTTP interceptor that handles 401 + claims challenge. | 2 | First `@RequireMfa()` route lands ([ADR-0011](decisions/0011-mfa-enforcement-entra-conditional-access.md)). | | **Audit-log inspection workflow** — querying `audit.events` as `audit_reader`, joining with app logs by `trace_id`, validating the append-only role grants in dev. | 2 | Audit module lands ([ADR-0013](decisions/0013-audit-trail-separated-postgres-append-only.md)). | | **Downstream API integration recipe** — adding a new `DownstreamApiConfig`, choosing the auth strategy (OBO vs service+assertion), wiring resilience policies, testing with a mocked downstream. | 2 | First downstream client lands ([ADR-0014](decisions/0014-downstream-api-access-obo-pattern.md)). | | **Component patterns library** — the in-house, spartan-style components (Angular CDK + Tailwind) as they ship, with a11y notes per component (keyboard model, ARIA, screen-reader expectations). | 5b suite | First non-placeholder component in `libs/shared/ui/`. | | **a11y testing workflow** — running axe-core via Playwright locally, screen-reader testing notes (NVDA / VoiceOver / TalkBack), the APF user-panel cadence and how to triage findings. | 3a | First Playwright e2e suite touching real screens ([ADR-0016](decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md)). | | **Performance debugging** — running Lighthouse CI locally with full config, reading the HTML reports, using `source-map-explorer` to investigate bundle bloat, interpreting BFF p95/p99 from OTel. | 3a | Lighthouse already wired in CI ([ADR-0017](decisions/0017-performance-budgets-lighthouse-ci.md)); section grows when first real route is added to the critical-routes list. | | **Debugging tips** — Angular DevTools, NestJS inspector, Prisma query log, OTel trace navigation, common gotchas. | cross | Accumulates organically as the team encounters them. | | **Release workflow** — tag-driven release, what `release.yml` does, version bumping, changelog generation from Conventional Commits. | 3b | On-prem infrastructure ADR + populated `release.yml`. | | **GitLab migration runbook** — when the org migrates Gitea → GitLab, how the workflows are ported, which level-2 sections of [ADR-0015](decisions/0015-cicd-gitea-actions.md) get superseded. | future | GitLab migration ADR (6–18 months horizon). | | **Architecture overview diagrams** — high-level component diagrams, data-flow diagrams, trust boundaries (for security review). | cross | First major architecture review or onboarding cohort ≥ 3 contributors. |