# 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`
