a20330a474
Pin the framework for calls from the BFF to integrated downstream APIs.
The concrete list of downstream services is not yet known, but the
framework must exist so that the day a developer adds an integration the
answer is 'use the standard client', not 'invent something'.
A DownstreamApisModule exposes a DownstreamApiClientFactory that produces
typed clients from per-service DownstreamApiConfig blocks. Each config
declares the auth strategy, base URL, timeout, retry, circuit breaker,
bulkhead, and audienceConstraint.
Default auth strategy for Entra-protected downstreams is On-Behalf-Of
(MSAL Node acquireTokenOnBehalfOf). Downstream-scoped tokens are cached
in Redis under obo:{user_id_hash}:{resource}, encrypted with AES-256-GCM
using a dedicated key (OBO_CACHE_ENCRYPTION_KEY) distinct from the
session-encryption key so a cache compromise doesn't cascade into a
session compromise.
Fallback strategy for non-Entra downstreams is service credential +
signed user-assertion header (X-User-Assertion JWT signed by the BFF's
private key, verified by downstreams against the BFF JWKS at
/.well-known/jwks.json). Token relay is rejected as a default;
per-user credential mapping is rejected outright.
Resilience composes via cockatiel: timeout outermost, then retry (only
on idempotent verbs and retriable error classes), circuit breaker per
service, bulkhead per service. Each call opens a downstream.<service>
OpenTelemetry span; auth failures emit audit events. Downstream errors
are translated at the client boundary - never bubbled with raw payload.
Audience pre-check is enforced at the call site (not at controller
entry) - even a missing authorization guard upstream cannot bypass the
audience constraint.
The framework is forward-looking; concrete integrations land per-service
in code config (no per-integration ADR unless the integration deviates
non-trivially from the defaults). Strategy code is exercised by
mock-driven tests until the first real integration ships.
decisions/README.md index updated. CLAUDE.md gains an explicit
'Downstream API access' line pointing to ADR-0014.
61 lines
5.3 KiB
Markdown
61 lines
5.3 KiB
Markdown
# Architectural Decision Records
|
|
|
|
This project records architecturally-significant decisions as **ADRs** in the [MADR 4.0.0](https://github.com/adr/madr) format. References: [adr.github.io](https://adr.github.io/).
|
|
|
|
## Why ADRs
|
|
|
|
ADRs capture the *why* behind a decision — context, drivers, options considered, trade-offs accepted — at the moment the decision is made. They make architecture reviewable, onboarding faster, and prevent the same debate from being re-litigated later.
|
|
|
|
## Conventions
|
|
|
|
- **Format:** MADR 4.0.0. Start from [template.md](template.md).
|
|
- **Filename:** `NNNN-kebab-case-title.md`, e.g. `0007-adopt-tailwind-for-design-tokens.md`.
|
|
- **Numbering:** globally sequential 4-digit prefix. Numbers never reset, never get reused — even when an ADR is superseded or deprecated.
|
|
- **Layout:** flat folder. ADRs are not nested into category subfolders; topical organization happens via tags.
|
|
- **Tags:** every ADR carries a `tags:` array in the MADR frontmatter, drawn from the [tag vocabulary](#tag-vocabulary) below. An ADR may carry several tags. Propose new tags (or renames) in the same PR that needs them; never invent ad-hoc tags inline.
|
|
- **Status lifecycle:** `proposed` → `accepted` → optionally `deprecated` or `superseded by [ADR-NNNN](NNNN-other.md)`. Update the YAML frontmatter; never delete an ADR.
|
|
- **Index maintenance:** every ADR addition or status change must update the [Index](#index) below in the same commit.
|
|
|
|
## When to write an ADR
|
|
|
|
Write one whenever a development decision is non-trivial: tool or library choice, framework pattern, security control, perf budget, a11y target, naming convention, deprecation, breaking change, or any choice that future contributors would benefit from understanding the *why* of.
|
|
|
|
## Tag vocabulary
|
|
|
|
The vocabulary below is the source of truth. It is intentionally coarse — propose extensions only when an existing tag genuinely doesn't fit, and avoid overly narrow tags.
|
|
|
|
| Tag | Scope |
|
|
| ---------------- | ----- |
|
|
| `frontend` | UI, Angular, components, design system, client-side state |
|
|
| `backend` | API, BFF, server-side services |
|
|
| `security` | AuthN, AuthZ, sessions, CSP, dependency scanning, secret management |
|
|
| `performance` | Perf budgets, caching, bundle size, Lighthouse |
|
|
| `accessibility` | WCAG, a11y testing, keyboard, ARIA, contrast |
|
|
| `infrastructure` | CI/CD, hosting, deployment, runtime |
|
|
| `observability` | Logs, metrics, traces, correlation IDs, monitoring |
|
|
| `data` | Persistence, schemas, migrations, data flow |
|
|
| `process` | Team conventions, workflows, repo policy |
|
|
|
|
> _Status: starter vocabulary, to be refined as ADRs accumulate. Update this table whenever a tag is added, renamed, or retired._
|
|
|
|
## Index
|
|
|
|
ADRs are listed in numerical order. To slice by topic, filter on the `Tags` column.
|
|
|
|
| # | Title | Status | Tags | Date |
|
|
| ---- | ----- | ------ | ---- | ---- |
|
|
| [0001](0001-use-adrs-to-record-architectural-decisions.md) | Use ADRs to record architectural decisions | accepted | `process` | 2026-04-29 |
|
|
| [0002](0002-adopt-nx-monorepo-apps-preset.md) | Adopt Nx monorepo with the `apps` preset | accepted | `infrastructure`, `frontend`, `backend` | 2026-04-29 |
|
|
| [0003](0003-workspace-and-app-naming-convention.md) | Workspace and app naming convention | accepted | `process` | 2026-04-29 |
|
|
| [0004](0004-frontend-stack-angular-csr-zoneless-signals.md) | Frontend stack — Angular (latest LTS), standalone, zoneless, Signals, CSR-only, Vitest | accepted | `frontend` | 2026-04-29 |
|
|
| [0005](0005-backend-stack-nestjs.md) | Backend stack — NestJS over Express, Fastify, Hono | accepted | `backend` | 2026-04-29 |
|
|
| [0006](0006-persistence-postgresql-prisma.md) | Persistence — PostgreSQL with Prisma | accepted | `data`, `backend` | 2026-04-29 |
|
|
| [0007](0007-pre-commit-hooks-and-conventional-commits.md) | Pre-commit hooks and Conventional Commits | accepted | `process` | 2026-04-29 |
|
|
| [0008](0008-identity-model-entra-workforce-dual-audience.md) | Identity model — multi-tenant Entra ID for workforce, dual-audience design for future External ID | accepted | `security`, `data` | 2026-04-29 |
|
|
| [0009](0009-auth-flow-oidc-pkce-msal-node.md) | Authentication flow — OIDC Authorization Code + PKCE via MSAL Node, BFF session pattern | accepted | `security`, `backend` | 2026-04-29 |
|
|
| [0010](0010-session-management-redis.md) | Session management — opaque session IDs in cookies, payload in self-hosted Redis with AES-GCM at rest | accepted | `security`, `backend`, `infrastructure` | 2026-04-29 |
|
|
| [0011](0011-mfa-enforcement-entra-conditional-access.md) | MFA enforcement — Entra ID Conditional Access baseline, BFF claim sanity-check, step-up hooks designed-in | accepted | `security` | 2026-04-29 |
|
|
| [0012](0012-observability-pino-opentelemetry.md) | Observability — Pino structured logs + OpenTelemetry tracing, W3C Trace Context propagation, stdout + collector | accepted | `observability`, `backend`, `frontend` | 2026-04-29 |
|
|
| [0013](0013-audit-trail-separated-postgres-append-only.md) | Audit trail — separated append-only Postgres schema, decoupled from app logs | accepted | `security`, `observability`, `data` | 2026-04-29 |
|
|
| [0014](0014-downstream-api-access-obo-pattern.md) | Downstream API access — On-Behalf-Of pattern, unified `DownstreamApiClient`, audience-aware authorization | accepted | `security`, `backend` | 2026-04-29 |
|