- docs/guides/dsr.md: GDPR Art. 15/16/17/18/20 interface mapping, tRPC router wiring, multi-subject handling, soft vs cascade-hard semantics, DeletionCertificate format and storage requirements - docs/guides/consent.md: requiresConsent manifest field, withConsent DI wiring, runtime isGranted pattern, IConsent audit trail, anonymous→ authenticated migration, cookie _v versioning, SSR-safe banner loading, CNIL/EDPB equal-prominence requirement - docs/compliance/subject-linkage.example.md: SubjectLink kind discriminator with worked support-ticket example (owner submitter + reference assignee) - docs/glossary.md: SubjectLink, DeletionCertificate, UserConsentState, ConsentChecked entries; Manifest definition updated with requiresConsent - CLAUDE.md: lint comment 8→12 conformance rules; conformance section notes requiresConsent; brand composition order updated to full 5-wrapper chain - docs/guides/conformance-quickref.md: requiresConsent field added to manifest table; component-must-have-story, component-must-have-test, atomic-tier-import-direction added to ESLint rules table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
190 lines
8.4 KiB
Markdown
190 lines
8.4 KiB
Markdown
# Data Subject Rights (DSR) guide
|
|
|
|
Consumer-facing reference for the `@repo/core-dsr` optional core package. Covers the four GDPR interfaces, tRPC procedure wiring, multi-subject collection handling, deletion semantics, `DeletionCertificate` format, and per-article compliance notes.
|
|
|
|
**Prerequisite:** scaffold the package if it isn't already present:
|
|
|
|
```bash
|
|
pnpm turbo gen core-package dsr
|
|
```
|
|
|
|
---
|
|
|
|
## Interfaces and GDPR article mapping
|
|
|
|
`@repo/core-dsr` exposes four vendor-neutral interfaces:
|
|
|
|
| Interface | tRPC procedure | GDPR article(s) | Mutation? |
|
|
| ------------------------ | -------------- | ----------------------------------------- | ---------- |
|
|
| `IDataExport` | `dsr.export` | Art. 15 (access) + Art. 20 (portability) | No (query) |
|
|
| `IDataDelete` | `dsr.delete` | Art. 17 (erasure / right to be forgotten) | Yes |
|
|
| `IDataRectify` | `dsr.rectify` | Art. 16 (rectification) | Yes |
|
|
| `IProcessingRestriction` | `dsr.restrict` | Art. 18 (restriction of processing) | Yes |
|
|
|
|
---
|
|
|
|
## Wiring the DSR router
|
|
|
|
`core-dsr` ships a pre-built tRPC router. Mount it once in your app router:
|
|
|
|
```ts
|
|
// apps/web-next/src/server/router.ts
|
|
import { createDsrRouter } from "@repo/core-dsr";
|
|
import { bindProductionDsr } from "@repo/core-dsr/di/bind-production";
|
|
|
|
const dsrBinding = bindProductionDsr({ config: payloadConfig, auditLog });
|
|
|
|
export const appRouter = t.router({
|
|
// ... other feature routers
|
|
dsr: createDsrRouter(dsrBinding),
|
|
});
|
|
```
|
|
|
|
All four procedures require an authenticated user in `ctx.user`. `cascade-hard` deletion additionally requires `ctx.user.roles` to include `"admin"`.
|
|
|
|
### Input schemas
|
|
|
|
| Procedure | Input fields |
|
|
| -------------- | ---------------------------------------------------------------------------- |
|
|
| `dsr.export` | `subjectId: string`, `format: "json" \| "json-ld"` |
|
|
| `dsr.delete` | `subjectId: string`, `mode: "soft" \| "cascade-hard"` |
|
|
| `dsr.rectify` | `subjectId: string`, `collection: string`, `field: string`, `value: unknown` |
|
|
| `dsr.restrict` | `subjectId: string`, `granted: boolean` |
|
|
|
|
---
|
|
|
|
## Multi-subject handling
|
|
|
|
A Payload collection can reference **multiple data subjects** — for example, a support ticket has both a submitter and an assignee. Declare subject relationships via `custom.subject` on the collection config:
|
|
|
|
```ts
|
|
// packages/<feature>/src/integrations/cms/support-tickets.collection.ts
|
|
{
|
|
slug: "support-tickets",
|
|
custom: {
|
|
subject: [
|
|
{ field: "submittedBy", kind: "self", target: "users" },
|
|
{ field: "assignedTo", kind: "reference", target: "users", role: "assignee" },
|
|
],
|
|
},
|
|
fields: [
|
|
{ name: "submittedBy", type: "relationship", relationTo: "users" },
|
|
{ name: "assignedTo", type: "relationship", relationTo: "users" },
|
|
],
|
|
}
|
|
```
|
|
|
|
The DSR cascade walks each `SubjectLink` at runtime to determine scope:
|
|
|
|
| `kind` | Meaning | Export behaviour | Delete behaviour |
|
|
| ------------- | ------------------------------------------------------------------------------------------ | ------------------------------------- | ---------------------------------- |
|
|
| `"self"` | The subject **is** this row (e.g., the Users row itself) | Full row in `asSelf` | Row deleted / pseudonymized |
|
|
| `"owner"` | The subject **created or owns** this row (e.g., posts authored by the user) | Full row in `asSelf` | Row deleted / pseudonymized |
|
|
| `"reference"` | The subject is **referenced** in this row but does not own it (e.g., assignee on a ticket) | Row ID + link coords in `asReference` | Linked field NULLed; row preserved |
|
|
|
|
See `docs/compliance/subject-linkage.example.md` for a full annotated example.
|
|
|
|
---
|
|
|
|
## Deletion modes
|
|
|
|
### `soft` (default, no admin role required)
|
|
|
|
Redacts or pseudonymizes PII fields in rows the subject owns (`kind: "self" | "owner"`) while preserving foreign-key integrity. Reference rows (`kind: "reference"`) have the linking field NULLed. The row structure remains intact — useful for preserving order history, audit trails, and referential integrity.
|
|
|
|
### `cascade-hard` (admin role required)
|
|
|
|
Hard-deletes all rows the subject owns (where `postDeletion.action === "hard-delete"` in the collection's retention config), then NULLs reference fields. Use only when the subject's right to erasure overrides your referential-integrity requirements or when retention policy mandates hard deletion.
|
|
|
|
Retention-policy overrides apply: a collection with `postDeletion.action = "pseudonymize"` will pseudonymize rather than hard-delete even in `cascade-hard` mode.
|
|
|
|
---
|
|
|
|
## `DeletionCertificate` format
|
|
|
|
`IDataDelete.deleteSubjectData(subjectId, mode)` resolves to a `DeletionCertificate`:
|
|
|
|
```ts
|
|
type DeletionCertificate = {
|
|
subjectId: string; // or "erased-{hash}" if the ID itself was purged
|
|
mode: "soft" | "cascade-hard";
|
|
timestamp: string; // ISO 8601
|
|
reason: "art-17-request" | "admin-expunge" | "retention-policy";
|
|
affected: Array<{
|
|
collection: string;
|
|
rowsAffected: number;
|
|
action: "deleted" | "redacted" | "pseudonymized";
|
|
fields?: string[]; // PII field names NULLed when action === "redacted"
|
|
}>;
|
|
auditEntryId: string; // links to the immutable audit log entry
|
|
};
|
|
```
|
|
|
|
**Storage requirements:** persist the `DeletionCertificate` permanently — it is your Art. 17 compliance evidence for regulatory inspection. The `auditEntryId` forms a tamper-evident chain back to the `core-audit` log. Never mutate a certificate after creation.
|
|
|
|
---
|
|
|
|
## `UserDataBundle` format (export)
|
|
|
|
`IDataExport.exportSubjectData(subjectId, format)` resolves to a `UserDataBundle`:
|
|
|
|
```ts
|
|
type UserDataBundle = {
|
|
subjectId: string;
|
|
exportedAt: string; // ISO 8601
|
|
format: "json" | "json-ld";
|
|
data: Record<
|
|
string,
|
|
{
|
|
// keyed by collection slug
|
|
asSelf?: Array<Record<string, unknown>>; // owned rows (PII-filtered)
|
|
asReference?: Array<{
|
|
rowId: string;
|
|
linkedField: string;
|
|
linkedThrough: string;
|
|
}>;
|
|
}
|
|
>;
|
|
auditLog?: AuditEntry[]; // subject-scoped audit history
|
|
"@context"?: string | Record<string, unknown>; // JSON-LD only
|
|
};
|
|
```
|
|
|
|
Only fields marked `exportable: true` in their `custom.pii` block are included in `asSelf` rows.
|
|
|
|
---
|
|
|
|
## Compliance notes
|
|
|
|
### Art. 15 — Right of access
|
|
|
|
`dsr.export` with `format: "json"` satisfies the right of access. Return the `UserDataBundle` directly in the API response or as a downloadable JSON file. Response time: ≤ 30 days under GDPR.
|
|
|
|
### Art. 16 — Right to rectification
|
|
|
|
`dsr.rectify` targets a single field. For bulk updates, call it once per field. The implementation calls `IDataRectify.rectifySubjectData(subjectId, collection, field, value)` and records an audit entry.
|
|
|
|
### Art. 17 — Right to erasure
|
|
|
|
`dsr.delete` with `mode: "soft"` is the default erasure path. Use `mode: "cascade-hard"` only when data minimisation obligations outweigh referential integrity needs. Both paths produce a `DeletionCertificate`.
|
|
|
|
**Exemptions:** Art. 17(3) allows retention for legal obligations (e.g., invoices, tax records). Implement exemptions via a `postDeletion.action = "pseudonymize"` retention override on the affected collection — the DSR cascade respects this automatically.
|
|
|
|
### Art. 18 — Restriction of processing
|
|
|
|
`dsr.restrict` with `granted: true` flags the subject's data for restricted processing. Your application code must check `IProcessingRestriction.isRestricted(subjectId)` before performing processing operations on restricted subjects.
|
|
|
|
### Art. 20 — Right to data portability
|
|
|
|
`dsr.export` with `format: "json-ld"` produces a machine-readable JSON-LD export suitable for portability. The `@context` field is populated with the schema URI for downstream consumption.
|
|
|
|
---
|
|
|
|
## Cross-references
|
|
|
|
- Subject linkage patterns: `docs/compliance/subject-linkage.example.md`
|
|
- PII field tagging: `docs/compliance/README.md` → "Annotating PII fields"
|
|
- Retention policy: `docs/compliance/retention-policy.example.yml`
|
|
- Audit log: `docs/guides/audit-and-compliance.md`
|
|
- Glossary: `SubjectLink`, `DeletionCertificate` in `docs/glossary.md`
|