# 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//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>; // owned rows (PII-filtered) asReference?: Array<{ rowId: string; linkedField: string; linkedThrough: string; }>; } >; auditLog?: AuditEntry[]; // subject-scoped audit history "@context"?: string | Record; // 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`