docs(decisions): add ADR-0018 — environment configuration strategy #80

Merged
julien merged 1 commits from docs/decisions/0018-env-config into main 2026-05-10 04:58:02 +02:00
3 changed files with 209 additions and 19 deletions
+1
View File
@@ -46,6 +46,7 @@ The structural, security, observability, and quality choices are recorded as ADR
- **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).
- **Local quality gates:** Husky + lint-staged + commitlint with Conventional Commits — see [ADR-0007](docs/decisions/0007-pre-commit-hooks-and-conventional-commits.md).
- **Runtime:** Node.js latest LTS major.
@@ -0,0 +1,188 @@
---
status: accepted
date: 2026-05-10
decision-makers: R&D Lead
tags: [frontend, backend, infrastructure, process]
---
# Environment configuration — Angular `environment.ts`, BFF env vars, audit pool split
## Context and Problem Statement
Both apps need per-environment configuration values — service endpoints, observability targets, feature flags, secrets — but the runtimes are radically different:
- The **BFF** is Node and naturally reads `process.env`. There is already a `.env.example` catalogue (see `apps/portal-bff/.env.example`) and a startup validator for `DATABASE_URL` ([`check-database-url.ts`](../../apps/portal-bff/src/config/check-database-url.ts)).
- The **SPA** is a pre-built static bundle served to the browser. There is no `process.env` at runtime, and the bundle is identical across dev / staging / production unless we build per-environment artefacts.
Two questions need a recorded answer before they bite the next time a feature lands:
1. **How does the SPA learn its environment-specific endpoints?** Today the OTLP collector URL and the BFF API URL are hard-coded in `apps/portal-shell/src/observability/tracing.ts` and `apps/portal-shell/src/app/pages/home/home-status.service.ts`. That works for `localhost` development, breaks the moment we ship to staging or production.
2. **How does the BFF connect to Postgres for the audit log?** ADR-0013 specifies the audit log uses a distinct Postgres role (`audit_writer`). v1 of the audit module ([`audit.service.ts`](../../apps/portal-bff/src/audit/audit.service.ts)) cuts a single connection from the shared `DATABASE_URL` pool and runs `SET LOCAL ROLE audit_writer` per transaction. ADR-0013 §"Wired as features land" already lists a separate `AUDIT_DATABASE_URL` pool as the production-grade hardening; this ADR records the actual contract so the implementation is unambiguous when it ships.
## Decision Drivers
- **Twelve-factor compliance** — config in the environment, not in the build artefact, _wherever feasible_.
- **No surprise drift between dev and prod** — same mechanism, different values; not "dev uses X, prod uses Y".
- **Simplicity for the contributor** — `cp .env.example .env`, edit, run. No config-discovery folklore.
- **Prod-faithful security** — the BFF connection that writes audit rows must not be the same connection that reads / writes the rest of the database. Defense in depth.
- **Stability bar** — recognised, battle-tested patterns. No bricolage.
## Considered Options
### SPA configuration mechanism
- **Angular `environment.ts` + `fileReplacements`** — a `src/environments/environment.ts` file holds the dev defaults; per-environment files (`environment.staging.ts`, `environment.prod.ts`) are swapped in at build time via the project.json's `fileReplacements`. Idiomatic Angular, supported by `@angular/build`. **(Chosen.)**
- **Runtime fetch from `/config.json`** — the SPA fetches a JSON file from the server at boot, before any feature code runs. Late-binding, single artefact across environments.
- **`<meta name="config" content="…">` injected in `index.html`** — the deploy step rewrites the `<meta>` content; the SPA reads `document.querySelector('meta[name=config]').content` at boot.
- **esbuild `define` substitution** — `process.env.X` references in source are textually replaced at build time; equivalent to `environment.ts` but less Angular-idiomatic.
### BFF audit-log connection
- **Single `DATABASE_URL` + `SET LOCAL ROLE audit_writer` per transaction** — current v1. One Prisma client, one pool. Role switch wraps each INSERT.
- **Separate `AUDIT_DATABASE_URL` pool with a login user that has `audit_writer` membership only** — second Prisma client, second pool. The connection cannot perform anything except `INSERT INTO audit.events`. **(Chosen for production hardening; v2 of the audit module.)**
- **Single connection that runs as `audit_writer` permanently** — everything goes through the audit-writer role. Wrong: the BFF needs many more privileges than `audit_writer` for the rest of its work.
### BFF env-var loading
- **`process.env` directly + a small validator at boot** — current pattern. Plain Node, no dep. Fail loud on missing or malformed values. **(Chosen, kept.)**
- **`@nestjs/config`** — Nest-idiomatic config module. Schema validation, hierarchical lookup, `.env` loading. Heavier surface for what amounts to ~10 keys today.
- **`zod` schema over `process.env`** — typed validation, narrow surface. Adds a runtime dep.
## Decision Outcome
### SPA — Angular `environment.ts` with build-time `fileReplacements`
- `apps/portal-shell/src/environments/environment.ts` is the **default** (dev), checked in. Holds two keys today: `bffApiBaseUrl` and `otlpEndpoint`. Both default to `http://localhost:3000/api` and `http://localhost:4318/v1/traces` respectively.
- `apps/portal-shell/src/environments/environment.staging.ts` and `apps/portal-shell/src/environments/environment.prod.ts` ship later — checked in, since they target real, public, non-secret URLs (the BFF and Collector are public services from the SPA's perspective).
- The Angular build target's `production` configuration in `project.json` adds a `fileReplacements` entry that swaps `environment.ts` for `environment.prod.ts`; a future `staging` configuration does the same with `environment.staging.ts`. Build script: `nx build portal-shell --configuration=production` continues to do the right thing without code changes.
- Anything **secret** (none today on the SPA side, by design) is _not_ in `environment.ts`. The SPA is a static bundle; secrets baked in are public secrets. If a feature ever requires a per-user / per-tenant secret, it lives in the session payload (per ADR-0010), never in the bundle.
Concrete migration of today's hard-coded values:
| Today | After |
| --------------------------------------------------------------------------- | ---------------------------------------- |
| `'http://localhost:4318/v1/traces'` in `observability/tracing.ts` | `environment.otlpEndpoint` |
| `'http://localhost:3000/api/health'` in `pages/home/home-status.service.ts` | `${environment.bffApiBaseUrl}/health` |
| `propagateTraceHeaderCorsUrls: [/^http:\/\/localhost:3000\/.*/]` | derived from `environment.bffApiBaseUrl` |
The runtime-fetch and `<meta>` alternatives are rejected: same artefact across environments is appealing but introduces a startup-blocking fetch (RUM cost) and a deploy-time HTML-rewrite step (extra moving piece). Build-time replacement is cheaper, idiomatic, and fits our infrastructure-as-code approach to deploy artefacts.
### BFF — `process.env` + boot-time validator, with explicit `AUDIT_DATABASE_URL` split
- BFF config keys remain plain `process.env['X']` reads, validated at boot in small dedicated files (the same shape as `apps/portal-bff/src/config/check-database-url.ts`). `@nestjs/config` is rejected: too much surface for a small key set; Nest's DI is not a real win when config is read once at boot.
- The validator family grows as feature ADRs land — `LOG_USER_ID_SALT` once auth is wired (ADR-0009 / ADR-0013), `SESSION_ENCRYPTION_KEY` and `SESSION_*_TIMEOUT_SECONDS` once sessions land (ADR-0010), `OBO_CACHE_ENCRYPTION_KEY` and `<SERVICE>_API_BASE_URL` once downstream wiring lands (ADR-0014). Each of those ADRs already mentions "BFF refuses to start without …" — that contract is implemented in this validator pattern.
- **`AUDIT_DATABASE_URL` is the production split**: when set, it overrides the shared `DATABASE_URL` for the audit module's Prisma client only. The connection string points at the same Postgres instance but uses a login user that is a member of `audit_writer` only — no superuser, no public-schema CRUD, just `INSERT INTO audit.events`. When **not** set (the dev default), the audit module falls back to the shared client + `SET LOCAL ROLE audit_writer` per-transaction (current v1 behaviour). The fallback explicit choice keeps the dev experience one-step (single `.env`, no role-membership setup beyond the existing init.sql), while guaranteeing production gets the harder isolation.
- A boot-time self-test asserts the split when `AUDIT_DATABASE_URL` is set: a deliberate `UPDATE audit.events SET …` is issued and is expected to fail with `permission denied`. If it succeeds, the BFF refuses to start — the audit log is no longer append-only.
- Renamed from the previous `.env.example` placeholder `AUDIT_DATABASE_URL (separate creds, role 'audit_writer')` — the contract is now the formal one above.
### Per-environment file structure
Both apps converge on the same naming for clarity:
```
apps/portal-shell/src/environments/
environment.ts ← dev, checked in
environment.staging.ts ← staging, checked in (later)
environment.prod.ts ← prod, checked in (later)
apps/portal-bff/.env.example ← single source-of-truth template, checked in
apps/portal-bff/.env ← per-developer values, git-ignored
```
Production BFF env values are managed by the deploy platform's secret manager (per the future on-prem infrastructure ADR — phase 3b); the `.env.example` stays the catalogue.
### Consequences
- Good, because every env-specific value has an obvious home and an obvious change procedure (edit `environment.{env}.ts` for SPA, edit secret-manager / `.env` for BFF).
- Good, because the SPA stays a static bundle — no runtime config-fetch latency, no extra deploy step beyond what `nx build` already does.
- Good, because the audit-pool split is now formalised. The migration from v1 (`SET LOCAL ROLE`) to v2 (separate pool) is a config change in production (set `AUDIT_DATABASE_URL`) and a small code change in `audit.module.ts` (instantiate a second client when the env var is set). Both are bounded.
- Good, because the boot-time validator pattern keeps the BFF crashing _early_ on misconfig — never a request-time surprise.
- Bad, because per-environment `environment.ts` files mean the SPA does not produce a single artefact reusable across environments. Every promotion through the deploy pipeline rebuilds. Acceptable: build is fast (~2 s) and the alternative (runtime config) was rejected on its own merits.
- Bad, because Angular's `fileReplacements` is a build-time mechanism that hides the substitution from the source — the contributor reading `environment.ts` does not see the prod values without opening `environment.prod.ts` and the project.json target. Mitigated by keeping all three files side by side in `src/environments/`.
- Neutral, because BFF config still uses raw `process.env` — no fancy schema validation. Acceptable while the key set stays under ~30; revisit with `zod` if it grows substantially.
### Confirmation
**Wired as part of the next feature work that touches per-environment config:**
- `apps/portal-shell/src/environments/environment.ts` (and per-environment siblings) created; `bffApiBaseUrl` and `otlpEndpoint` exposed.
- The hard-coded URLs in `observability/tracing.ts` and `pages/home/home-status.service.ts` consume the environment object instead.
- `apps/portal-shell/project.json` `production` configuration declares the `fileReplacements` swap; a `staging` configuration follows when the staging build target is set up.
- BFF gains a `apps/portal-bff/src/config/audit-database-url.ts` validator that reads `AUDIT_DATABASE_URL` (optional) and exposes a typed config; the `AuditModule` instantiates a second Prisma client when the var is set.
- Boot-time UPDATE-rejection self-test runs against the audit-writer pool when configured.
- `apps/portal-bff/.env.example` documents `AUDIT_DATABASE_URL` per the contract above (replacing the placeholder line).
**Wired as the corresponding feature ADRs ship (already documented in their own §Confirmation, repeated here for indexing):**
- `LOG_USER_ID_SALT` mandatory at boot — auth ADR-0009 / sessions ADR-0010 / observability ADR-0012.
- `SESSION_ENCRYPTION_KEY`, `SESSION_IDLE_TIMEOUT_SECONDS`, `SESSION_ABSOLUTE_TIMEOUT_SECONDS` — sessions ADR-0010.
- `MFA_FRESHNESS_SECONDS` — MFA ADR-0011.
- `OBO_CACHE_ENCRYPTION_KEY`, `BFF_JWKS_PRIVATE_KEY_PATH`, `BFF_JWKS_KID`, `<SERVICE>_API_BASE_URL` — downstream API ADR-0014.
## Pros and Cons of the Options
### SPA configuration mechanism
#### Angular `environment.ts` + `fileReplacements` (chosen)
- Good, because idiomatic Angular — every Angular contributor recognises it.
- Good, because zero runtime cost, zero extra deploy step.
- Good, because the values are visible in source (one file per environment, side by side).
- Bad, because per-environment artefact (rebuild on promotion). Acceptable given build is fast.
#### Runtime fetch from `/config.json`
- Good, because single artefact across environments (build once, deploy everywhere).
- Bad, because adds a startup-blocking HTTP fetch (RUM cost on `LCP`/`TTFB` budgets per ADR-0017).
- Bad, because a runtime-config error means the SPA cannot start — discoverable only at first user load.
#### `<meta name="config" content="…">` injected at deploy
- Good, because also single artefact.
- Bad, because the deploy platform must rewrite `index.html` as part of release. Extra moving piece, easy to forget on a new platform.
- Bad, because mixing config and HTML hurts cacheability of `index.html`.
#### esbuild `define` substitution
- Good, because also build-time, also fast.
- Bad, because circumvents Angular's expected pattern; future contributors look for `environment.ts` first. Avoid the surprise.
### BFF audit-log connection
#### Separate `AUDIT_DATABASE_URL` pool (chosen for prod, opt-in)
- Good, because hard runtime isolation — the audit-writing connection cannot perform anything else.
- Good, because a SQL injection on a non-audit code path cannot reach the audit log.
- Bad, because second connection pool to operate (sized, monitored, drained on shutdown). Bounded operational cost.
#### Single `DATABASE_URL` + `SET LOCAL ROLE` (current v1)
- Good, because one pool, one set of credentials. Simpler dev story.
- Bad, because the BFF connection retains all its other privileges; a runtime exploit could `RESET ROLE` and bypass the contract. Acceptable for v1, mitigated by Postgres role grants ensuring `UPDATE` / `TRUNCATE` are denied even to the underlying superuser at runtime — but the BFF login user could still read business data the audit-only connection wouldn't.
#### Single connection that runs as `audit_writer` permanently
- Bad, because the BFF's other features need many more privileges than `audit_writer`. Doesn't fit.
### BFF env-var loading
#### `process.env` + boot-time validator (chosen)
- Good, because zero dep, fast, clear failure.
- Bad, because no schema declaration in one place — validation is per-file.
#### `@nestjs/config`
- Good, because Nest-idiomatic, hierarchical lookup.
- Bad, because heavy for our key count, adds DI ceremony, hides the simplicity of "just read process.env".
#### `zod` schema
- Good, because typed validation, clear schema declaration.
- Bad, because runtime dep for an at-boot check that does not need to be re-run.
## More Information
- Angular `fileReplacements`: https://angular.dev/tools/cli/build#configuration-environment-specific
- Twelve-factor config: https://12factor.net/config
- Related ADRs: [ADR-0006](0006-persistence-postgresql-prisma.md) (Postgres + Prisma), [ADR-0010](0010-session-management-redis.md) (sessions), [ADR-0012](0012-observability-pino-opentelemetry.md) (OTLP endpoint env var), [ADR-0013](0013-audit-trail-separated-postgres-append-only.md) (audit pool, this ADR formalises its split), [ADR-0014](0014-downstream-api-access-obo-pattern.md) (per-downstream env vars).
+20 -19
View File
@@ -42,22 +42,23 @@ The vocabulary below is the source of truth. It is intentionally coarse — prop
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 |
| [0015](0015-cicd-gitea-actions.md) | CI/CD pipeline — Gitea Actions, trunk-based + squash-merge, thin YAML over portable scripts | accepted | `infrastructure`, `process` | 2026-04-30 |
| [0016](0016-accessibility-baseline-wcag-aa-targeted-aaa.md) | Accessibility baseline — WCAG 2.2 AA + targeted AAA, Angular CDK + spartan-ng + Tailwind, APF panel testing | accepted | `accessibility`, `frontend`, `process` | 2026-04-30 |
| [0017](0017-performance-budgets-lighthouse-ci.md) | Performance budgets — Core Web Vitals + Lighthouse CI gates, bundle budgets, BFF p95/p99 SLOs | accepted | `performance`, `frontend`, `backend`, `process` | 2026-04-30 |
| # | 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 |
| [0015](0015-cicd-gitea-actions.md) | CI/CD pipeline — Gitea Actions, trunk-based + squash-merge, thin YAML over portable scripts | accepted | `infrastructure`, `process` | 2026-04-30 |
| [0016](0016-accessibility-baseline-wcag-aa-targeted-aaa.md) | Accessibility baseline — WCAG 2.2 AA + targeted AAA, Angular CDK + spartan-ng + Tailwind, APF panel testing | accepted | `accessibility`, `frontend`, `process` | 2026-04-30 |
| [0017](0017-performance-budgets-lighthouse-ci.md) | Performance budgets — Core Web Vitals + Lighthouse CI gates, bundle budgets, BFF p95/p99 SLOs | accepted | `performance`, `frontend`, `backend`, `process` | 2026-04-30 |
| [0018](0018-environment-configuration-strategy.md) | Environment configuration — Angular `environment.ts`, BFF env vars, audit pool split | accepted | `frontend`, `backend`, `infrastructure`, `process` | 2026-05-10 |