289a5bad09
CI / scan (pull_request) Successful in 2m31s
CI / commits (pull_request) Successful in 2m31s
CI / check (pull_request) Successful in 2m40s
CI / a11y (pull_request) Successful in 1m51s
Docs site / build (pull_request) Successful in 2m3s
CI / perf (pull_request) Successful in 6m11s
The development guide drifted as portal-admin shipped, the docs site
landed, and Prisma stayed pinned at 6.x. Surgical pass.
Section 1 (Repo layout):
* Add `apps/portal-admin/` and `apps/portal-admin-e2e/` — both
exist on `main` since #134.
* Drop the phantom `prisma.config.ts` (Prisma 6.x doesn't ship
one) and fix the "Prisma 7" tag to "Prisma 6.x" with the
ADR-0006 pin reference.
* Add `docs/index.md`, `docs/architecture.md`, `docs/.vitepress/`
+ new workflows (`docs-site.yml`, `renovate.yml`) so the layout
matches what `ls` actually shows.
Section 3 (Initial setup) — new diagram:
* Mermaid `flowchart` of the local-dev topology (host-side dev
servers + Compose containers + viewer profiles + the optional
docs site). Replaces the prose ports-table that was scattered
across §3 and §5 for new contributors.
Section 4 (Daily commands):
* Add `portal-admin` to serve / test / generate examples.
* Correct the single-test-file recipe — Nx's vitest executor
rejects `--testFile`; use positional `path/to/file.spec.ts`
for Vitest projects, `--testPathPattern=…` for Jest.
* New "Documentation site" subsection — `docs:dev` / `docs:build`
/ `docs:preview`, link to ADR-0022.
Section 5 (Observability):
* Mermaid `sequenceDiagram` of how a click becomes a trace
(browser span → traceparent → BFF span → Pino log line with
matching trace_id → OTLP batch → Jaeger). Anchors the
correlation rule that the prose explained but didn't visualise.
Section 6 (Renovate):
* Add "Transitive vulnerabilities — `pnpm.overrides`" subsection.
Documents the pattern from #159 (vite + esbuild via VitePress)
so the next contributor hitting a transitive vuln has the
playbook on hand.
Section 9 (Sections to come):
* Split into "Code shipped — doc to write" and "Not yet". Many
phase-2 items moved out of "to come" since the code has shipped
(auth, sessions, MFA decorator, admin module, audit viewer,
user directory, downstream strategies, OpenAPI tooling,
capabilities endpoint). The roadmap now reflects what's
actually still missing.
577 lines
42 KiB
Markdown
577 lines
42 KiB
Markdown
# 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/01-wsl-terminal-setup.md](setup/01-wsl-terminal-setup.md).
|
||
|
||
---
|
||
|
||
## 1. Repo layout
|
||
|
||
```
|
||
apf_portal/
|
||
├── .gitea/workflows/ # CI pipelines (ADR-0015)
|
||
│ ├── ci.yml # per-PR + push to main: check / scan / commits / perf / a11y
|
||
│ ├── docs-site.yml # build docs site on PR + push (ADR-0022)
|
||
│ ├── renovate.yml # daily Renovate run (cron 03:00 UTC)
|
||
│ └── 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 (incl. /logos)
|
||
│ │ ├── src/ # entry, app config, routes, styles, locales
|
||
│ │ ├── .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-admin/ # Angular 21 SPA (admin surface, distinct origin + session per ADR-0020)
|
||
│ │ ├── public/ # static assets (incl. /logos)
|
||
│ │ ├── src/ # entry, app config, routes, audit + users + profile pages
|
||
│ │ └── project.json
|
||
│ ├── portal-admin-e2e/ # Playwright e2e for portal-admin
|
||
│ ├── portal-bff/ # NestJS 11 BFF (Express adapter, ValidationPipe, Jest)
|
||
│ │ ├── src/ # main, app module, controllers, services
|
||
│ │ ├── prisma/schema.prisma # Prisma 6.x schema (PostgreSQL) — pin documented in ADR-0006
|
||
│ │ ├── .env.example # env-vars catalog (committed); .env stays gitignored
|
||
│ │ └── project.json
|
||
│ └── portal-bff-e2e/ # Jest e2e for portal-bff
|
||
├── libs/
|
||
│ ├── feature/<name>/ # vertical feature libs (e.g. feature-auth)
|
||
│ └── shared/<scope>/ # cross-cutting libs (state, tokens, ui, util)
|
||
├── docs/
|
||
│ ├── README.md # doc index (git/IDE side — excluded from the docs site)
|
||
│ ├── index.md # VitePress home (Hero layout, see ADR-0022)
|
||
│ ├── architecture.md # cross-cutting Mermaid diagrams (C4, module boundaries, CI)
|
||
│ ├── development.md # this file
|
||
│ ├── decisions/ # ADRs (MADR 4.0.0)
|
||
│ ├── setup/ # local-environment onboarding (Zsh, pnpm, Nx workspace)
|
||
│ └── .vitepress/ # VitePress config + build cache (gitignored)
|
||
├── 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 (incl. pnpm.overrides for transitive remediation)
|
||
├── pnpm-workspace.yaml # apps/* + libs/**
|
||
├── renovate.json # Renovate config (groupings, dashboard, vuln alerts)
|
||
├── 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-<name>`, `shared-<scope>`)
|
||
- [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".
|
||
|
||
### Topology at a glance
|
||
|
||
How the pieces fit together once the stack is up — three Node dev servers on the host, the infrastructure containers behind Docker Compose, optional viewer profiles for inspection:
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
subgraph host["Dev host (WSL2 / Linux / macOS)"]
|
||
direction TB
|
||
shell["portal-shell<br/><code>:4200</code>"]
|
||
admin["portal-admin<br/><code>:4300</code>"]
|
||
bff["portal-bff<br/><code>:3000</code>"]
|
||
docs["docs site<br/><code>:5173</code><br/>(VitePress)"]
|
||
end
|
||
|
||
subgraph compose["infra/local/ (Docker Compose)"]
|
||
direction TB
|
||
pg[("Postgres<br/><code>:5432</code>")]
|
||
redis[("Redis<br/><code>:6379</code>")]
|
||
otel["OTel Collector<br/><code>:4318</code>"]
|
||
subgraph obs["observability profile"]
|
||
jaeger["Jaeger UI<br/><code>:16686</code>"]
|
||
end
|
||
subgraph dbt["dbtools profile"]
|
||
pgweb["pgweb<br/><code>:8081</code>"]
|
||
end
|
||
end
|
||
|
||
shell -. "<code>/api/*</code>" .-> bff
|
||
admin -. "<code>/api/admin/*</code>" .-> bff
|
||
bff --> pg
|
||
bff --> redis
|
||
bff -- "OTLP" --> otel
|
||
shell -- "OTLP/HTTP<br/>spans" --> otel
|
||
admin -- "OTLP/HTTP<br/>spans" --> otel
|
||
otel --> jaeger
|
||
pgweb --- pg
|
||
```
|
||
|
||
The docs site lives off to the side — it has no runtime dependency on the BFF or the infra stack and only reads files under `docs/`. Same goes for the SPAs from `portal-shell` to `portal-admin`: each runs its own dev server with its own port, talks to the same BFF, but holds a **distinct session cookie** (per [ADR-0020](decisions/0020-portal-admin-app.md) §"Sessions — distinct from `portal-shell`").
|
||
|
||
---
|
||
|
||
## 4. Daily commands
|
||
|
||
### Run the apps
|
||
|
||
```bash
|
||
pnpm nx serve portal-shell # http://localhost:4200 (Angular dev server)
|
||
pnpm nx serve portal-admin # http://localhost:4300 (Angular dev server)
|
||
pnpm nx serve portal-bff # http://localhost:3000/api (NestJS)
|
||
```
|
||
|
||
All three can run in parallel in separate terminals; the SPAs proxy API calls to the BFF in dev. Each SPA holds its own cookie / session — signing in to one does not authenticate the other (per [ADR-0020](decisions/0020-portal-admin-app.md)).
|
||
|
||
### Test
|
||
|
||
```bash
|
||
pnpm nx test portal-shell # Vitest (single run; --configuration=watch for watch mode)
|
||
pnpm nx test portal-admin # Vitest
|
||
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 test file:
|
||
|
||
- **Vitest projects** (`portal-shell`, `portal-admin`, `feature-auth`, `shared-*`): use a positional path filter — `pnpm nx test portal-shell --skip-nx-cache src/app/app.spec.ts`. `--testFile` is not accepted by the Nx vitest executor (`'testFile' is not found in schema`).
|
||
- **Jest projects** (`portal-bff`): `pnpm nx test portal-bff --testPathPattern=src/auth/auth.service`.
|
||
|
||
### 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 (specify the SPA — `portal-shell` or `portal-admin`)
|
||
pnpm nx g @nx/angular:component <name> --project=portal-shell --standalone
|
||
pnpm nx g @nx/angular:component <name> --project=portal-admin --standalone
|
||
|
||
# Service / controller / module in portal-bff
|
||
pnpm nx g @nx/nest:service <name> --project=portal-bff
|
||
pnpm nx g @nx/nest:controller <name> --project=portal-bff
|
||
|
||
# New shared lib (TS-only, consumable by both apps)
|
||
pnpm nx g @nx/js:library --name=shared-<scope> --directory=libs/shared/<scope> \
|
||
--bundler=tsc --unitTestRunner=vitest \
|
||
--tags="scope:shared,type:shared" --no-interactive
|
||
|
||
# New Angular feature lib (front-only — usable by either SPA)
|
||
pnpm nx g @nx/angular:library --name=feature-<name> --directory=libs/feature/<name> \
|
||
--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.
|
||
|
||
### Documentation site
|
||
|
||
`docs/**/*.md` renders as a VitePress site per [ADR-0022](decisions/0022-docs-site-vitepress.md). No Nx project for it — pure pnpm scripts:
|
||
|
||
```bash
|
||
pnpm docs:dev # http://localhost:5173 (HMR — edit a .md, see it live)
|
||
pnpm docs:build # static site → docs/.vitepress/dist/
|
||
pnpm docs:preview # serve the built site locally to smoke-test
|
||
```
|
||
|
||
Adding a new ADR is a single-file change: drop `docs/decisions/NNNN-kebab-title.md` (MADR 4.0.0 frontmatter, see [`template.md`](decisions/template.md)) and the sidebar auto-lists it. Update [`decisions/README.md`](decisions/README.md) in the same PR so the curated tag-grouped index stays in sync.
|
||
|
||
### 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 <migration-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.
|
||
|
||
### How a click becomes a trace
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
actor U as User
|
||
participant SPA as portal-shell
|
||
participant BFF as portal-bff
|
||
participant Pino as BFF stdout (Pino)
|
||
participant OC as OTel Collector
|
||
participant J as Jaeger UI
|
||
|
||
U->>SPA: click
|
||
Note over SPA: span `user_interaction`<br/>(root, generates trace_id)
|
||
SPA->>BFF: fetch /api/foo<br/>traceparent: 00-<trace_id>-<span_id>-01
|
||
Note over SPA: span `fetch` (child)
|
||
Note over BFF: span `HTTP GET /api/foo`<br/>(continues same trace)
|
||
BFF->>Pino: log line<br/>`{ ...payload, trace_id, span_id, request_id }`
|
||
BFF-->>SPA: 200 OK
|
||
SPA-)OC: OTLP/HTTP batch (every ~5 s)
|
||
BFF-)OC: OTLP batch
|
||
OC-)J: forward spans
|
||
Note over J: trace_id matches the one in Pino<br/>full nested timeline visible
|
||
```
|
||
|
||
The single point of correlation between the two pillars is **`trace_id`**: Jaeger's URL bar carries it (`/trace/<hex>`), and `@opentelemetry/instrumentation-pino` injects the same value on every BFF log line emitted inside that request's scope. Grep one, navigate the other.
|
||
|
||
### 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":"<the hex>"' /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 <PID>`.
|
||
|
||
### 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 <code v-pre>${{ github.\* }}</code> 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.
|
||
|
||
### Transitive vulnerabilities — `pnpm.overrides`
|
||
|
||
Renovate's vulnerability remediation handles **direct** `package.json` deps. For **transitive** vulnerabilities (a vulnerable package pulled in by a healthy direct dep), Renovate stays silent in the dashboard — the v40 image doesn't write `pnpm.overrides` automatically. The manual pattern lives in [`package.json`](../package.json) → `pnpm.overrides`, version-selector form:
|
||
|
||
```jsonc
|
||
"esbuild@<0.25.0": ">=0.25.0",
|
||
"vite@<6.4.2": ">=6.4.2",
|
||
```
|
||
|
||
Two properties of this form worth knowing:
|
||
|
||
1. **The override is gated on the vulnerable range** — it only kicks in when the resolved version is < the threshold, becoming a no-op the day the underlying dep's own bump ships a patched version.
|
||
2. **pnpm overrides bypass peer constraints** — if the parent dep declares `vite ^5.0.0` but no patched 5.x exists, pnpm will pick a patched 6.x/7.x. Verify the chantier still builds before merging the override.
|
||
|
||
The first instance of this pattern is preserved in [#159](https://git.unespace.com/julien/apf_portal/pulls/159) (vite + esbuild via VitePress 1.6.4). Next time `pnpm audit` fails with a transitive vuln and Renovate is silent, this is the playbook.
|
||
|
||
---
|
||
|
||
## 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 — `<type>(<scope>): <description>`, 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
|
||
|
||
As features for later phases land, the corresponding sections below are filled in directly. Entries are split between **code shipped — doc to write** (the work is on `main`, only the prose is missing) and **not yet** (neither code nor doc).
|
||
|
||
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.
|
||
|
||
### Code shipped — doc to write
|
||
|
||
| Future section | Code shipped in | What the section will cover |
|
||
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||
| **Auth dev-loop** | [ADR-0009](decisions/0009-auth-flow-oidc-pkce-msal-node.md) implementation series (PRs #100s) | Microsoft 365 Developer tenant configuration, MSAL Node connection, OIDC code-flow walkthrough, switching between dev and prod-like tenants. |
|
||
| **Session inspection** | [ADR-0010](decisions/0010-session-management-redis.md) implementation | Reading the Redis session store in dev (`redis-cli`), decrypting the AES-GCM `tokens` blob with the dev `SESSION_ENCRYPTION_KEY`, force-logout patterns. |
|
||
| **MFA step-up debugging** | `@RequireMfa()` decorator + freshness guard (#128) | Triggering claims-challenge flows when the first consumer route lands, verifying `amrAt` freshness, testing the SPA interceptor on 401 + claims challenge. _(Decorator exists; no consumer route yet — section lands together with that.)_ |
|
||
| **Admin surface workflows** | [ADR-0020](decisions/0020-portal-admin-app.md) skeleton (#127) + audit viewer (#136) + user directory (#140–#142) | Walking through `/audit` and `/users` viewers, role assignment in Entra (`Portal.Admin`), cross-app navigation. |
|
||
| **Audit-log inspection workflow** | [ADR-0013](decisions/0013-audit-trail-separated-postgres-append-only.md) + admin viewer | Querying `audit.events` as `audit_reader`, joining with Pino logs by `trace_id` / `actor_id_hash`, validating the append-only role grants in dev. |
|
||
| **Downstream API integration recipe** | [ADR-0014](decisions/0014-downstream-api-access-obo-pattern.md) strategies (OBO #137, signed-assertion + JWKS #138/#139) | Adding a new `DownstreamApiConfig`, choosing the auth strategy, wiring resilience policies, testing with a mocked downstream. _(Strategy layer ready; no v1 consumer yet — section grows with the first.)_ |
|
||
| **OpenAPI / Scalar dev workflow** | [#143](https://git.unespace.com/julien/apf_portal/pulls/143) | Browsing `/api/docs`, annotating new controllers with `@ApiTags` / `@ApiOperation`, exporting the spec for Bruno / Postman, the CSRF caveat when hitting mutating routes from Scalar. |
|
||
| **Capabilities-driven SPA UX** | `/api/me/capabilities` + cross-app menu links (#151) | When to add a new key to `CapabilitiesView`, how the SPA's `CapabilitiesService` consumes it, the "binary boolean, never the raw role" rule. |
|
||
|
||
### Not yet
|
||
|
||
| Future section | Triggered by |
|
||
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
||
| **Component patterns library** — in-house spartan-style components (Angular CDK + Tailwind), a11y notes per component (keyboard model, ARIA, screen-reader expectations). | First non-placeholder component in `libs/shared/ui/` beyond `Icon` and `UserMenu`. |
|
||
| **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. | 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. | First real route added to the critical-routes list ([ADR-0017](decisions/0017-performance-budgets-lighthouse-ci.md)). |
|
||
| **Debugging tips** — Angular DevTools, NestJS inspector, Prisma query log, OTel trace navigation, common gotchas. | Accumulates organically as the team encounters them. |
|
||
| **Release workflow** — tag-driven release, what `release.yml` does, version bumping, changelog generation from Conventional Commits. | 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. | GitLab migration ADR (6–18 months horizon). |
|