diff --git a/docs/README.md b/docs/README.md index ac96a0a..5d70a6d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,6 +15,10 @@ This is the entry point to all project documentation. It is maintained automatic - [development.md](development.md) — repo layout, prerequisites, initial setup, daily commands, conventional commit cycle. Day-to-day reference for working on the project. +### Architecture + +- [architecture.md](architecture.md) — cross-cutting Mermaid diagrams: C4 system context, C4 containers, Nx module boundaries, CI/CD pipeline. Single-decision diagrams (auth sequence, ERD, etc.) live inline in their ADR. + ### Onboarding & environment Setup guides for new contributors: @@ -23,10 +27,6 @@ Setup guides for new contributors: - [setup/02-dev-web-stack.md](setup/02-dev-web-stack.md) — Node via nvm, pnpm via corepack, Docker - [setup/03-angular-nx-monorepo.md](setup/03-angular-nx-monorepo.md) — Angular + Nx monorepo bootstrap -### Architecture - -_Empty — to be populated as the project grows._ - ### Operations & runbooks _Empty — to be populated when we deploy._ diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..5cf3ecd --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,196 @@ +# Architecture diagrams + +Visual cross-cuts of `apf_portal`'s architecture. Each diagram summarises decisions that are spread across several ADRs and exists to help a contributor (or auditor, RSSI, IT contact) build a mental model fast. Decisions themselves live in [decisions/](decisions/); diagrams reflect the current accepted state. + +Diagrams are written in [Mermaid](https://mermaid.js.org/) — text in markdown, rendered natively by Gitea, GitHub, and most IDE markdown viewers. Updating a diagram is an ordinary text edit reviewable in PR. + +When a diagram is the _content_ of a single decision (e.g. a sequence diagram that captures a flow described by one ADR), it lives **inside that ADR**, not here. This file holds the _cross-cutting_ views. + +--- + +## 1. System context (C4 level 1) + +The portal as a black box, with the actors that interact with it and the external systems it depends on. + +Sources: [ADR-0008](decisions/0008-identity-model-entra-workforce-dual-audience.md) (identity model), [ADR-0014](decisions/0014-downstream-api-access-obo-pattern.md) (downstream APIs), [ADR-0013](decisions/0013-audit-trail-separated-postgres-append-only.md) (audit access). + +```mermaid +flowchart TB + workforce([Workforce user
employee]) + customer([Customer user
external
future]) + it([IT / Identity admin]) + rssi([RSSI / SOC analyst]) + + portal[apf_portal
web portal centralising
access to existing apps] + + entra[(Microsoft Entra ID
workforce tenant
+ M365 Developer dev tenant)] + extid[(Microsoft Entra External ID
customer tenant
future)] + downstream[(Downstream APIs
existing applications
integrated by the portal)] + + workforce -->|browser HTTPS| portal + customer -.->|browser HTTPS
future| portal + portal -->|OIDC Auth Code + PKCE| entra + portal -.->|OIDC Auth Code + PKCE
future| extid + portal -->|OBO or signed assertion| downstream + it -->|configures
tenant + Conditional Access
+ B2B invitations| entra + rssi -->|reads audit events
via audit_reader role| portal + + classDef future stroke-dasharray: 5 5,opacity:0.7 + class customer,extid future +``` + +Dashed = future scope (not v1). The customer audience is _designed for_ but not implemented (per the dual-audience design in ADR-0008); it is shown to make the future expansion legible. + +--- + +## 2. Containers (C4 level 2) + +The runtime artefacts and their conversations. One step deeper than system context: what is actually deployed. + +Sources: [ADR-0002](decisions/0002-adopt-nx-monorepo-apps-preset.md) (Nx layout), [ADR-0004](decisions/0004-frontend-stack-angular-csr-zoneless-signals.md) (Angular SPA), [ADR-0005](decisions/0005-backend-stack-nestjs.md) (NestJS BFF), [ADR-0006](decisions/0006-persistence-postgresql-prisma.md) (Postgres + Prisma), [ADR-0009](decisions/0009-auth-flow-oidc-pkce-msal-node.md) (auth flow), [ADR-0010](decisions/0010-session-management-redis.md) (sessions in Redis), [ADR-0012](decisions/0012-observability-pino-opentelemetry.md) (OTel collector), [ADR-0014](decisions/0014-downstream-api-access-obo-pattern.md) (downstream calls). + +```mermaid +flowchart TB + user([Workforce user]) + + subgraph Browser["Browser"] + spa["portal-shell
Angular 21 SPA
zoneless / Signals / Tailwind 4"] + end + + subgraph OnPrem["On-prem deployment"] + bff["portal-bff
NestJS 11 / Node 24
Express adapter"] + pg[("Postgres 17
schemas: public + audit
RLS for dual-audience")] + redis[("Redis
sessions + OBO token cache
AES-256-GCM at rest")] + otel["OTel Collector
local sidecar
(OTLP → backend, future)"] + end + + entra[(Microsoft Entra ID)] + downstream[(Downstream APIs)] + + user -->|HTTPS| spa + spa -->|"HTTPS
__Host-portal_session
X-CSRF-Token
traceparent"| bff + spa -. "OTLP / HTTP
(browser spans)" .-> otel + bff -->|"OIDC Auth Code + PKCE
via @azure/msal-node"| entra + bff -->|Prisma 7| pg + bff -->|"ioredis
(opaque session id
→ encrypted blob)"| redis + bff -->|"OBO token (Entra-protected)
or signed X-User-Assertion JWT
+ traceparent"| downstream + bff -. "OTLP / HTTP
(server spans + Pino logs)" .-> otel +``` + +Notes embedded in the diagram: + +- The SPA carries no token. Only the opaque session id cookie (`__Host-portal_session`) plus the CSRF cookie (`__Host-portal_csrf`, read by JS for double-submit). +- `traceparent` (W3C) propagates from the browser through the BFF to the downstream APIs — same `trace_id` end-to-end. +- The OTel Collector is the only piece coupled to the eventual on-prem observability backend. Choice of backend (Grafana Loki + Tempo, ELK, …) is deferred to the on-prem infrastructure ADR. + +--- + +## 3. Nx module boundaries + +Which projects are allowed to depend on which. Encoded in `eslint.config.mjs` via `@nx/enforce-module-boundaries` with the `depConstraints` from [ADR-0003](decisions/0003-workspace-and-app-naming-convention.md). + +Each project carries two tags: + +- **`scope:`** — `portal-shell`, `portal-bff`, or `shared` (for libs consumable by both apps). +- **`type:`** — `app`, `feature`, or `shared`. + +A dependency is allowed only if both axes permit it. + +```mermaid +flowchart LR + subgraph Apps["apps · type:app"] + shell["portal-shell
scope:portal-shell"] + bff["portal-bff
scope:portal-bff"] + end + + subgraph FeatureLibs["libs · type:feature"] + fauth["feature-auth
scope:portal-shell"] + end + + subgraph SharedShellLibs["libs · type:shared · scope:portal-shell"] + sui["shared-ui
Angular components
(spartan-style on CDK)"] + end + + subgraph SharedAnyLibs["libs · type:shared · scope:shared"] + stokens["shared-tokens
design tokens (a11y)"] + sutil["shared-util
cross-runtime TS helpers"] + end + + shell --> fauth + shell --> sui + shell --> stokens + shell --> sutil + + bff --> stokens + bff --> sutil + + fauth --> sui + fauth --> stokens + fauth --> sutil + + sui --> stokens + + classDef forbidden stroke:#c00,stroke-dasharray: 4 4 +``` + +Forbidden by the depConstraints (and lint-enforced) — examples: + +- `portal-bff` ⟶ `shared-ui` (back imports Angular code: scope clash). +- `portal-bff` ⟶ `feature-auth` (back imports a frontend feature). +- `shared-tokens` ⟶ `shared-ui` (a `type:shared` lib cannot reach a `type:shared` lib in a narrower scope, nor a feature lib). +- Any lib ⟶ any app. + +--- + +## 4. CI/CD pipeline + +How a change moves from a developer's keyboard to `main`. Reflects [ADR-0007](decisions/0007-pre-commit-hooks-and-conventional-commits.md) (local hooks), [ADR-0015](decisions/0015-cicd-gitea-actions.md) (Gitea Actions, trunk-based + squash-merge), [ADR-0016](decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md) (a11y gate), [ADR-0017](decisions/0017-performance-budgets-lighthouse-ci.md) (perf gate). + +```mermaid +flowchart TB + edit[Edit on feat/*, fix/*, chore/*, docs/* branch] + pre[".husky/pre-commit
lint-staged → prettier on staged"] + msg[".husky/commit-msg
commitlint → Conventional Commits"] + push[git push origin feat/*] + pr[Open PR → main] + + subgraph PRJobs["CI on PR · .gitea/workflows/ci.yml · all blocking"] + direction LR + j1["check
nx affected -t
format / lint / test / build"] + j2["scan
pnpm audit + Trivy + gitleaks"] + j3["commits
commitlint on PR range"] + j4["perf
Lighthouse CI
CWV thresholds (ADR-0017)"] + j5["a11y
axe-core via Playwright
(placeholder until first screens)"] + end + + protection{"Branch protection on main
· all 5 jobs green
· ≥0 reviewers (v1, →≥1 with 2nd contributor)
· linear history
· no direct push, no force push"} + squash[Squash-merge
subject = Conventional Commits
(becomes the commit on main)] + tag[Tag vX.Y.Z] + release[".gitea/workflows/release.yml
(stub today, populated with on-prem deploy ADR)"] + + edit --> pre --> msg --> push --> pr + pr --> PRJobs --> protection -->|all green| squash --> tag --> release + + cron["Weekly cron · Mon 04:00 UTC"] + sched[".gitea/workflows/security-scheduled.yml
full-tree Trivy + gitleaks
+ Lighthouse on prod (when LHCI_PROD_URL set)"] + cron --> sched +``` + +Note the parallelism: the five PR jobs run **in parallel**. The diagram shows the gate as one square because branch protection requires _all five_ before squash-merge is allowed. + +--- + +## To be added + +As features land, the following diagrams will be added — either here, or inline in the ADR they belong to (per the convention stated at the top: _cross-cutting → here, single-decision → in the ADR_). + +| Diagram | Where it will land | Triggered by | +| ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------- | +| **OIDC Auth Code + PKCE sequence** | inline in [ADR-0009](decisions/0009-auth-flow-oidc-pkce-msal-node.md) | already useful — added at the same time as this file | +| **Trace context propagation** (SPA → BFF → DB → downstream) | here | first observability instrumentation lands | +| **Dual-audience flow** (token validation, claim → enum, RLS filtering) | here or split between ADRs 0008/0009/0013 | first authz code that touches the audience | +| **Step-up MFA flow** (claims challenge round-trip) | inline in [ADR-0011](decisions/0011-mfa-enforcement-entra-conditional-access.md) | first `@RequireMfa()` route | +| **Database ERD** | inline in [ADR-0006](decisions/0006-persistence-postgresql-prisma.md) or a dedicated `data.md` | first business model in `schema.prisma` | +| **Audit event lifecycle** (writer → store → archiver → reader) | inline in [ADR-0013](decisions/0013-audit-trail-separated-postgres-append-only.md) | audit module shipped | +| **Downstream call lifecycle** (audience pre-check → strategy → cache → resilience → trace) | inline in [ADR-0014](decisions/0014-downstream-api-access-obo-pattern.md) | first downstream integration | +| **Trust boundaries** (security review view) | here | when the RSSI requests a security architecture review |