- 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>
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,DeletionCertificateindocs/glossary.md