Files
agentic-dev-template/docs/guides/dsr.md
Danijel Martinek a3505f2e69 docs(compliance): add DSR guide, consent guide, subject-linkage example, glossary terms
- 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>
2026-05-19 22:07:50 +00:00

8.4 KiB

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:

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:

// 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:

// 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:

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:

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