## 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; } interface IDataDelete { deleteSubjectData( subjectId: string, mode: "soft" | "cascade-hard", ): Promise; } interface IDataRectify { updateSubjectField( subjectId: string, collection: string, field: string, value: unknown, ): Promise; } interface IProcessingRestriction { setRestriction(subjectId: string, granted: boolean): Promise; isRestricted(subjectId: string): Promise; } ``` 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; grant( subjectId: string, categories: ConsentCategory[], record: ConsentRecord, ): Promise; withdraw(subjectId: string, categories: ConsentCategory[]): Promise; getCategories(subjectId: string): Promise; } ``` `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" })`). **`` 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; } ``` - 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 `. - **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)