## ADR-025 — EU compliance baseline (DPA/GDPR template scope)

**Status:** Accepted
**Date:** 2026-05-18
**Related:** ADR-006 (vertical feature packages), ADR-017 (OTel + observability PII boundary), ADR-018 (audit + compliance), ADR-022 (library evaluation policy — extended here for sub-processors), ADR-023 (CI security + supply chain — SBOM amended here), ADR-024 (product analytics channel)
**Companion PRDs** (one per epic, sequenced):

- `docs/work/prds/compliance-manifests-pii-retention-subprocessors.prd.md` (Epic A)
- `docs/work/prds/dsr-consent-and-cookie-banner.prd.md` (Epic B)
- `docs/work/prds/security-headers-rate-limit-sbom.prd.md` (Epic C)
- `docs/work/prds/compliance-docs-scaffolds.prd.md` (Epic D)

## Context

A DPA/GDPR compliance playbook (22 sections, stack-agnostic) was reviewed against the template's current state on **2026-05-18**. The audit produced a clean three-way split:

1. **Already covered** by prior ADRs:
   - PII boundary on observability (ADR-017 §7 — `sendDefaultPii: false` CI gate, server-side scrubbing, replay masking)
   - Audit logging baseline (ADR-018 — `@repo/core-audit`, DPA-aligned schema, `eraseSubject` pseudonymization)
   - EU library residency (ADR-022 — hard filter in `/evaluate-library`)
   - Supply-chain + CI security (ADR-023 — Renovate SHA pinning, Socket.dev, audit signatures, CodeQL, gitleaks, trace revalidation)
   - Analytics PII deferral (ADR-024 — explicit consumer-policy boundary)

2. **Template-shaped gap** — 10 items the playbook flagged that the template can codify _before_ any consumer adopts it. These are conformance-pattern shaped (manifest fields, brands, ESLint rules, generators) or scaffolding (interfaces, default middleware, fill-in docs).

3. **Product-shaped (deferred)** — 3 items that need product shape before being meaningful. Each has a documented trigger condition for revisit.

The motivating pressure: a consumer adopting this template today gets ~50% of the playbook's surface for free. The 10-item template-shaped gap is what this ADR plans (raising coverage to ~80%); the remaining ~20% product/process/legal scope stays consumer-owned and is documented as such.

Audit + DSR is the canonical confusion to flag upfront: **audit _records_ personal-data access (immutable journal); DSR _acts_ on the underlying data in response to user requests (mutator/exporter).** They are sibling concerns, not duplicates.

## Decision

Ship the 10 template-shaped items across **four epics**, sequenced A → B → D with C interleaved opportunistically. Defer 3 items explicitly. Add 3 new manifest fields, 2 new optional cores, 3 new ESLint rules.

### The 4 epics

#### Epic A — Declarative compliance manifests

**Items:** PII inventory + data retention + sub-processor inventory.

Three declarative artifacts, each driven by a different surface and a generator that emits the audit-evidence YAML:

| Artifact                | Declaration site                                                                                                                                          | Generator output                  |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
| PII inventory           | `custom.pii: { category, purpose, retention, exportable, restrictable }` per Payload **field**                                                            | `compliance/data-map.yml`         |
| Retention policy        | `custom.retention: { activeRetention, postDeletion, purgeSchedule, hardDeleteAfter }` per Payload **collection**                                          | `compliance/retention-policy.yml` |
| Sub-processor inventory | Extended ADR-022 library traces — frontmatter fields `is-sub-processor`, `processes-pii`, `data-sent`, `region`, `dpa-signed`, `sccs-required`, `contact` | `compliance/sub-processors.yml`   |

**Key design decisions:**

- **PII at the field level, not the manifest level.** PII is a storage question ("what personal data does this system hold?"), not an action question. Existing manifest fields (`audits`, `publishes`, `analyticsEvents`) are all event-shaped — emissions from use cases. PII fields don't fit that shape. Tagging at the field level puts the metadata where DSR consumes it at runtime.

- **Retention at the collection level, not per use case.** Same rationale — retention is a storage property. Per-field PII retention overrides apply where the PII tag specifies a stricter retention than the collection default (more-specific wins). Background purge job in `core-shared/jobs` reads collection config at boot.

- **Sub-processors via ADR-022 traces, not standalone file.** Every direct-dep library trace already records EU residency, license, CVE acceptance. Extending it with the sub-processor fields (DPA signed date, SCCs, contact, PII processed, data sent, region) unifies two related obligations in one record. Pure-HTTP sub-processors (REST calls without an SDK) get hand-authored entries with no backing trace as an exception, CI-flagged.

The three generators run via `pnpm compliance:emit-all`. CI gate verifies generator output matches the source declarations (drift detection).

**Background purge job:** `core-shared/jobs/retention-purge.job.ts`. Reads `custom.retention` from each collection at boot; schedules per-collection purge cadence; emits an `IAuditLog.record({ action: "DELETE", reason: "retention-policy" })` audit entry per row purged.

#### Epic B — DSR + consent + cookie banner

**Items:** DSR scaffold + consent abstraction + cookie consent UI.

Builds the user-rights surface end-to-end:

**`@repo/core-dsr`** — new optional core. Four interfaces:

```ts
interface IDataExport {
  exportSubjectData(
    subjectId: string,
    format: "json" | "json-ld",
  ): Promise<UserDataBundle>;
}
interface IDataDelete {
  deleteSubjectData(
    subjectId: string,
    mode: "soft" | "cascade-hard",
  ): Promise<DeletionCertificate>;
}
interface IDataRectify {
  updateSubjectField(
    subjectId: string,
    collection: string,
    field: string,
    value: unknown,
  ): Promise<void>;
}
interface IProcessingRestriction {
  setRestriction(subjectId: string, granted: boolean): Promise<void>;
  isRestricted(subjectId: string): Promise<boolean>;
}
```

DSR ops walk Payload collections at runtime, using the field-level `custom.pii` tags from Epic A. `IDataExport` walks fields tagged `exportable: true`; `IDataDelete` walks all PII fields and cascades; `IProcessingRestriction` writes a flag on the user record that every read path checks.

**Scope cuts on DSR:**

- Art. 20 (portability) folded into Art. 15 (access) — same `IDataExport` with format option
- Art. 21 (objection) → consent epic via `IConsent.withdraw`
- Art. 22 (automated decision-making) → deferred (no ML in template; future ADR when a consumer adds automated decisions)

DSR ops are themselves PII access events — every `IDataExport`/`IDataDelete`/`IDataRectify` call writes an audit entry. After `IDataDelete`, `core-audit.IAuditLog.eraseSubject(actorId, "pseudonymize")` scrubs the audit trail (preserves the events, removes the identifier).

**`@repo/core-consent`** — new optional core. Sibling channel parallel to audit/analytics:

```ts
interface IConsent {
  isGranted(subjectId: string, category: ConsentCategory): Promise<boolean>;
  grant(
    subjectId: string,
    categories: ConsentCategory[],
    record: ConsentRecord,
  ): Promise<void>;
  withdraw(subjectId: string, categories: ConsentCategory[]): Promise<void>;
  getCategories(subjectId: string): Promise<ConsentCategory[]>;
}
```

`ConsentCategory` is a consumer-typed string-literal-union (default: `"essential" | "functional" | "analytics" | "marketing"`, extensible). Conformance treatment:

- Brand: `ConsentChecked` attached by `withConsent` wrapper at bind time
- Manifest field: `requiresConsent: ["analytics"]` per use case
- ESLint rule: `no-undeclared-consent-check` cross-checks `consent.requires("X")` literal calls
- Boot assertion: `assertFeatureConformance` requires `ConsentChecked` brand when `requiresConsent.length > 0`

Consent grant/withdraw events are themselves audited (`auditLog.record({ action: "CONSENT_GRANT", category: "marketing" })`).

**`<CookieConsentBanner>` in `@repo/core-ui`** — atomic component. Default visual treatment baked for EU prominence requirements (Reject + Accept side-by-side, same size, equal prominence per EU regulator guidance). Granular (essential/functional/analytics/marketing). Consumer wires `IConsent` via React context. Storybook story doubles as human reading-room for compliant UX.

**Endpoint scaffolds:** `/api/gdpr/{export,delete,rectify,restrict}` — consumer-wireable routes. Live in `apps/web-next/app/api/gdpr/` and `apps/web-tanstack/src/routes/api/gdpr/`.

#### Epic C — Security hardening

**Items:** Security headers middleware + rate-limit primitive + SBOM in CI.

Small individual scope; grouped for dispatch efficiency. Each item is independent.

- **Security headers middleware** — default Next.js + TanStack middleware shipping HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy. CSP customizable per consumer (separate config; default is restrictive). Lives in `core-shared/security` plus per-framework re-export.

- **Rate-limit primitive** — fourth conformance channel after audit/analytics/consent. `IRateLimit` interface in `core-shared/rate-limit`:

  ```ts
  interface IRateLimit {
    consume(
      key: string,
      weight?: number,
    ): Promise<{ allowed: boolean; remaining: number; resetAt: Date }>;
    reset(key: string): Promise<void>;
  }
  ```

  - Brand: `RateLimited` attached by `withRateLimit` wrapper
  - Manifest field: `rateLimit: { window: "1m", budget: 60 }` per use case (defaults; runtime overrides via `ctx.rateLimit` config)
  - ESLint rule: `no-undeclared-rate-limit` warns when a use case in an auth/write/export category lacks a `rateLimit` declaration
  - Boot assertion: `assertFeatureConformance` requires `RateLimited` brand when `rateLimit` is set
  - Consumer wires Redis/Upstash impl; `NoopRateLimit` always-allows for tests + dev

  Rate-limit budgets at the manifest level are _defaults_ — overridable at runtime via `ctx.rateLimit` config (mirrors how analytics backend is consumer-wired). Manifest declaration is for the binding gate; runtime values are deployment-environment-specific.

- **SBOM in CI** — `cyclonedx-npm` step in `ci.yml`, artifact uploaded per release. Amendment to ADR-023.

#### Epic D — Compliance docs scaffolds

**Items:** Fill-in templates for runbooks, policies, and the pre-launch checklist.

Pure docs work. Lands last so it references the manifest fields, interfaces, and middleware shipped by A/B/C.

**Template-shipped (under `docs/compliance/`):**

- `data-map.example.yml` — generator-output reference
- `retention-policy.example.yml` — generator-output reference
- `sub-processors.example.yml` — generator-output reference
- `incident-runbook.template.md` (fill-in)
- `dsr-procedure.template.md`
- `backup-policy.template.md`
- `password-policy.template.md`
- `device-policy.template.md`
- `onboarding.template.md`
- `offboarding.template.md`
- `README.md` (explains the `docs/compliance/` vs `compliance/` split)

**Consumer-created (under `compliance/` at repo root):**

- `data-map.yml` (generator output)
- `retention-policy.yml` (generator output)
- `sub-processors.yml` (generator output from extended ADR-022 traces)
- `*.md` (filled-in copies of templates)

**Plus:** `docs/guides/pre-launch-compliance-checklist.md` — playbook §19 verbatim with template-specific wiring noted.

### Sequencing

**Order: A → B → D, with C interleaved opportunistically.**

Hard dependencies:

- B's `IDataExport`/`IDataDelete` walk Epic A's PII tags at runtime → A must finish before B story 1
- D's `data-map.example.yml` documents Epic A's PII schema → A's design must be settled before D
- D's `dsr-procedure.template.md` references Epic B's endpoints → B should be design-settled before D's PRD is decomposed

C is dependency-free; sandcastle picks C stories during gaps in A/B/D.

### Deferrals (explicit, with revisit triggers)

| Deferred                                                                                 | Why deferred                                                                                                                        | Trigger to revisit                                                 |
| ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| **RBAC primitive** (roles + permissions + tenant scoping)                                | Needs product-side decisions: which roles exist, multi-tenant or not, permission granularity                                        | First downstream consumer ships with a stable role model           |
| **MFA + password policy + lockout** (`auth` feature extension)                           | Needs identity-infrastructure choices (TOTP/WebAuthn), threat-model-specific policy values, OTP delivery vendor (ADR-022 territory) | First downstream consumer establishes auth threat model            |
| **Breach detection patterns** (failed-login burst, bulk-access anomaly, off-hours admin) | Needs real auth flows, analytics backend, on-call infrastructure, product-specific anomaly definitions                              | First downstream consumer has live traffic + observability backend |
| **GDPR Art. 22** (automated decision-making) — sub-deferral within Epic B                | Template has no ML/automated decisions                                                                                              | First downstream consumer adds automated decisions                 |

Each deferral has a documented trigger so the decision-when can be answered by the consumer, not the template authors.

### Consumer-scope items (explicitly out of template)

These appear in the playbook but are NOT template-shaped:

- **Infrastructure (§1, §12)** — EU region pinning of compute/storage/backups, TLS at deploy edge, encryption-at-rest config, VPN/bastion network boundaries, backup strategy + restore testing
- **Legal (§17)** — DPA, Privacy Policy, ToS, SCCs for non-EU sub-processors, DPIA artifacts, RoPA documents
- **Organizational (§14, §15)** — MDM enrollment, HR onboarding/offboarding scripts (the _script_ is template; the _policy_ is consumer), NDAs, security training, background checks, quarterly access reviews, pentest scheduling

Epic D ships fill-in templates for some documentation artifacts above; the values stay consumer-filled.

### Manifest schema impact

Per-use-case fields grow from 5 to 7:

- Existing: `mutates`, `audits`, `publishes`, `consumes`, `analyticsEvents`
- Added by ADR-025: `requiresConsent`, `rateLimit`

Per-Payload-collection `custom` config grows:

- Added by ADR-025: `pii` (per field), `retention` (per collection)

Per-library-trace frontmatter grows (extends ADR-022):

- Added by ADR-025: `is-sub-processor`, `processes-pii`, `data-sent`, `region`, `dpa-signed`, `sccs-required`, `contact`

### Conformance ESLint rule impact

Rule count: 7 → 10. New rules at warn severity (matching the audit/analytics-event convention):

- `no-undeclared-consent-check` — `consent.requires(...)` literal calls must match manifest's `requiresConsent`
- `no-undeclared-rate-limit` — auth/write/export categorized use cases without `rateLimit` field
- `pii-declaration-must-be-complete` — Payload `pii: true` fields missing required sub-keys (category, purpose, retention)

### Generator + CI gate inventory

| Generator                          | Source                                                      | Output                            | CI gate                                      |
| ---------------------------------- | ----------------------------------------------------------- | --------------------------------- | -------------------------------------------- |
| `pnpm compliance:data-map`         | Payload field `custom.pii`                                  | `compliance/data-map.yml`         | output matches collections (drift detection) |
| `pnpm compliance:retention-policy` | Payload collection `custom.retention`                       | `compliance/retention-policy.yml` | output matches collections                   |
| `pnpm compliance:sub-processors`   | `docs/library-decisions/*.md` with `is-sub-processor: true` | `compliance/sub-processors.yml`   | output matches traces                        |
| `pnpm compliance:emit-all`         | runs all three                                              | three files                       | runs all three CI gates                      |

## Alternatives considered

### A. One mega-epic covering all 10 items

Single PRD, single epic, ~30-40 stories. Pros: tight coupling across the manifest-schema changes; one review surface. Cons: enormous PR backlog with no natural checkpoint; reviewer fatigue; if half-shipped, the partial state leaves an ambiguous compliance surface.

**Rejected.** The four-epic split keeps each PRD focused enough for a useful single-document review.

### B. Per-item ADRs (10 ADRs)

One ADR per item — finer granularity. Pros: each architectural decision recorded in isolation. Cons: 10 ADRs to maintain, much repetition (each restates the playbook context), no unifying strategy doc, harder to answer cross-cutting questions like "why these 3 deferrals" without re-reading 4+ ADRs.

**Rejected.** ADR-025 is the unifying strategy; per-epic PRDs handle implementation specifics. Future deepening decisions on individual items can spawn their own ADRs as needed (e.g., when DSR cascade semantics need a specific architectural call, that becomes ADR-NNN).

### C. Wait until the first downstream consumer asks

Don't build any of this until a consumer arrives with a real compliance requirement.

**Rejected.** The whole value proposition of this template is that DPA/GDPR-shaped consumers don't have to invent these surfaces. The "named-consumer-now" rule from ADR-022 applies to _library adoption_, not to template surface — the consumer of this surface is "every downstream EU-bound product," which is real and immediate.

### D. PII declared at use-case manifest level instead of Payload field level

`pii: [{ category, purpose, retention }]` per use case, mirroring audit/publishes.

**Rejected.** Duplicates metadata across every use case touching the same field. Wrong semantic layer — PII is a storage question, not an action question. DSR (Epic B) needs runtime access to PII tags at the field level to walk Payload collections; manifest-level tags would require synthesis at runtime.

### E. DSR split across multiple cores (`@repo/core-data-export`, `@repo/core-data-delete`, etc.)

Maximum granularity. Pros: consumers adopt only what they need. Cons: 4+ packages to scaffold, tight coupling in practice (delete cascade needs to know export's PII tags), overkill — each "core" would have one interface.

**Rejected.** One `@repo/core-dsr` with 4 interfaces, mirroring `core-shared`'s tracer/logger/metrics packaging pattern (multiple interfaces in one package). Opt-in is at the package level, not the interface level.

### F. Consent folded into `core-audit` or `core-analytics`

`IConsent` added to an existing core.

**Rejected.** Audit _records_, consent _gates_ — different abstractions. Conflates package purpose. Analytics is only one of many consent-gated channels (marketing, profiling, cookies, third parties); putting consent inside analytics is too narrow.

### G. Rate-limit as interface-only (no brand)

Just an `IRateLimit` contract. Consumers call `rateLimit.consume(...)` where they need.

**Rejected.** Rate-limit drift would only surface when traffic hits — way too late. The five-latency drift detection is the template's signature pattern. Skipping it for rate-limit when it's universally applicable to auth/write/export endpoints (per playbook §5) leaves a real hole. Manifest declaration at the binding gate + runtime budget override gives both static enforcement and deployment flexibility.

### H. Compliance docs all under `docs/compliance/` (no root `compliance/` directory)

Templates and live artifacts co-located.

**Rejected.** The template ships only the _shape_; the consumer fills in the _evidence_. Auditors expect `compliance/` at the repo root (matches playbook §16 + standard SOC 2/ISO 27001 audit prep). Splitting locations matches the template-vs-consumer mental model used throughout this session.

## Consequences

### Positive

- **Compliance surface ~80% template-shipped.** From ~50% pre-ADR (audit + PII boundary + library residency + supply chain) to ~80% post-epics. Remaining 20% is consumer-scope by design.
- **Conformance pattern extended consistently.** Two new manifest fields (`requiresConsent`, `rateLimit`) plus two new collection-level `custom.*` extensions all follow the established pattern. Three new ESLint rules at warn level. Three new manifest-driven generators. New consumers learn the pattern once.
- **DPA audit posture improves materially.** Sub-processor inventory, retention policy, data map, DSR endpoints, cookie consent are concrete artifacts a regulator or customer audit expects.
- **Explicit deferrals prevent premature design.** RBAC, MFA, breach detection, Art. 22 won't be re-suggested by future agents — ADR-025 records the trigger conditions.
- **Two new optional cores** (`core-dsr`, `core-consent`) match the established pattern. Template-tiers grows by two; scaffold path is `pnpm turbo gen core-package <name>`.
- **Rate-limit gets first-class treatment.** Fourth conformance channel; auth/write/export endpoints can't ship without a declared budget.
- **The audit ↔ DSR distinction is documented.** Future agents won't conflate the two — glossary entries plus this ADR's "Context" section make the split explicit.

### Negative

- **Manifest schema grows substantially.** Per-use-case fields go from 5 to 7. Per-collection `custom` config gains two extensions. Doc burden in glossary + `conformance-quickref.md` increases proportionally.
- **Conformance ESLint rule count: 7 → 10.** CLAUDE.md and quickref need rule-table updates each epic.
- **Two new optional cores to maintain.** Each needs versioning + CHANGELOG (per ADR-021).
- **`compliance/` directory becomes a new root location.** Adds a top-level directory alongside `docs/`, `packages/`, `apps/`. Consumers will see it; it's intentional but it's a new convention.
- **DSR cascade is non-trivial.** `IDataDelete` walking every Payload collection's PII fields requires Epic A's PII tags to be complete and correct. Epic B will surface gaps in Epic A's coverage during integration.
- **ADR-022 amendment.** Adding sub-processor fields to library traces is an extension to ADR-022's frontmatter spec. Existing traces need backfill (the weekly revalidation cron from ADR-023 will surface incomplete traces).
- **ADR-023 amendment.** SBOM generation step adds one workflow line; minor but counted.

### Neutral

- **No new CI gates beyond what generators introduce.** The three drift-detection gates (data-map, retention-policy, sub-processors) all reuse the existing CI workflow shape.
- **No vendor lock-in.** All interfaces (DSR cascade target, consent backend, rate-limit backend, security-header CSP values) remain consumer-decisions. Template ships interfaces + Noop/reference impls only.
- **Deferred items remain deferred.** ADR-025 doesn't preclude building RBAC/MFA/breach-detection/Art. 22 later — it just establishes that those decisions wait for product shape.
- **Cookie banner ships with opinionated EU-prominence defaults.** Consumers can override visual treatment but the default is the legally-defensible shape.

## Related

- ADR-006 — vertical feature packages (boundary tags new optional cores fit within)
- ADR-017 — OTel + observability PII boundary (the boundary `IConsent` does NOT cross — observability stays id-only)
- ADR-018 — audit + compliance (the sibling channel `core-dsr` complements without overlapping)
- ADR-022 — library evaluation policy (extended here with sub-processor frontmatter fields)
- ADR-023 — CI security + supply chain (SBOM amended here; rate-limit complements the supply-chain stack)
- ADR-024 — product analytics channel (sibling capture channel; `IConsent` from Epic B will gate `IAnalytics.track` calls in consumer products)
