- 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>
171 lines
5.6 KiB
Markdown
171 lines
5.6 KiB
Markdown
# Subject linkage — annotated example
|
|
|
|
This document describes the `custom.subject` declaration pattern for Payload collections that store data belonging to **more than one data subject**. It serves as the anchor for downstream consumers adding PII-holding collections to the data map.
|
|
|
|
For background on the DSR cascade that consumes these declarations, see `docs/guides/dsr.md`.
|
|
|
|
---
|
|
|
|
## What is a `SubjectLink`?
|
|
|
|
A `SubjectLink` is a field-level declaration that maps a Payload relationship field to a data subject. The DSR cascade reads these at runtime to determine:
|
|
|
|
1. Which rows to include in an Art. 15 export (`dsr.export`).
|
|
2. Which rows to delete or redact in an Art. 17 erasure (`dsr.delete`).
|
|
|
|
```ts
|
|
type SubjectLink = {
|
|
field: string; // Payload field name (must be a relationship field)
|
|
kind: "self" | "owner" | "reference";
|
|
target?: string; // Slug of the auth collection this field relates to
|
|
role?: string; // Semantic label (informational, appears in the data map)
|
|
};
|
|
```
|
|
|
|
### `kind` discriminator
|
|
|
|
| `kind` | Meaning |
|
|
| ------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
| `"self"` | The field **is** the subject row — used on the auth collection itself (e.g., Users) |
|
|
| `"owner"` | The subject **created or owns** this row (e.g., the author of a post) |
|
|
| `"reference"` | The subject is **referenced** in this row but does not own it (e.g., an assignee, a reviewer, a mentioned user) |
|
|
|
|
The difference between `"self"` and `"owner"` matters for deletion: rows marked `"self"` are the subject's account rows; rows marked `"owner"` are authored/owned content. Both are treated as owned data for Art. 15/17 purposes. `"reference"` rows are never deleted — only the linking field is NULLed.
|
|
|
|
---
|
|
|
|
## Worked example: support ticket collection
|
|
|
|
A support ticket has two subject relationships — the user who submitted it and the support agent assigned to it.
|
|
|
|
```ts
|
|
// packages/<feature>/src/integrations/cms/support-tickets.collection.ts
|
|
import type { CollectionConfig } from "payload";
|
|
|
|
export const SupportTicketsCollection: CollectionConfig = {
|
|
slug: "support-tickets",
|
|
custom: {
|
|
subject: [
|
|
{
|
|
field: "submittedBy",
|
|
kind: "owner", // The submitter owns this ticket
|
|
target: "users",
|
|
role: "submitter",
|
|
},
|
|
{
|
|
field: "assignedTo",
|
|
kind: "reference", // The assignee is merely linked, not the owner
|
|
target: "users",
|
|
role: "assignee",
|
|
},
|
|
],
|
|
retention: {
|
|
purgeSchedule: "daily",
|
|
postDeletion: {
|
|
action: "pseudonymize",
|
|
duration: "P30D",
|
|
trigger: "after-deletion",
|
|
},
|
|
},
|
|
},
|
|
fields: [
|
|
{
|
|
name: "title",
|
|
type: "text",
|
|
},
|
|
{
|
|
name: "body",
|
|
type: "textarea",
|
|
custom: {
|
|
pii: {
|
|
category: "user-generated-content",
|
|
purpose: ["service-delivery"],
|
|
exportable: true,
|
|
restrictable: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "submittedBy",
|
|
type: "relationship",
|
|
relationTo: "users",
|
|
required: true,
|
|
},
|
|
{
|
|
name: "assignedTo",
|
|
type: "relationship",
|
|
relationTo: "users",
|
|
},
|
|
],
|
|
};
|
|
```
|
|
|
|
### What the DSR cascade does with this collection
|
|
|
|
**Art. 15 export** for user `alice`:
|
|
|
|
- `submittedBy === alice` → ticket rows appear in `data["support-tickets"].asSelf` (filtered to exportable PII fields: `body`).
|
|
- `assignedTo === alice` → ticket row IDs + link coordinates appear in `data["support-tickets"].asReference`.
|
|
|
|
**Art. 17 soft delete** for user `alice`:
|
|
|
|
- Rows where `submittedBy === alice` → `body` field NULLed (pseudonymized, per `postDeletion.action = "pseudonymize"`).
|
|
- Rows where `assignedTo === alice` → `assignedTo` field NULLed; row is otherwise untouched.
|
|
|
|
**Art. 17 cascade-hard delete** for user `alice` (admin only):
|
|
|
|
- Rows where `submittedBy === alice` and `postDeletion.action` resolves to `"hard-delete"` → rows deleted entirely.
|
|
- Rows where `assignedTo === alice` → `assignedTo` field NULLed (reference rows are never hard-deleted — they belong to the submitter).
|
|
|
|
---
|
|
|
|
## Collections with a single subject
|
|
|
|
For collections owned by a single subject (e.g., user profile rows, blog posts), a single `SubjectLink` suffices:
|
|
|
|
```ts
|
|
custom: {
|
|
subject: [
|
|
{ field: "author", kind: "owner", target: "users" },
|
|
],
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## The Users collection (`kind: "self"`)
|
|
|
|
The auth collection itself uses `kind: "self"` to mark the primary subject identifier field:
|
|
|
|
```ts
|
|
// This is scaffolded automatically when `auth: true` is set on the collection.
|
|
custom: {
|
|
subject: [
|
|
{ field: "id", kind: "self", target: "users" },
|
|
],
|
|
}
|
|
```
|
|
|
|
The DSR cascade treats `kind: "self"` rows as the subject's account record — deletion here is always gated behind the strictest policy.
|
|
|
|
---
|
|
|
|
## Regenerating the data map
|
|
|
|
After adding or changing `custom.subject` declarations, regenerate the compliance data map:
|
|
|
|
```bash
|
|
pnpm compliance:data-map
|
|
```
|
|
|
|
The output at `compliance/data-map.yml` shows every collection + its subject links + its PII fields. The pre-commit hook and CI gate run `compliance:data-map --check` to detect uncommitted drift.
|
|
|
|
---
|
|
|
|
## Cross-references
|
|
|
|
- DSR procedures and deletion semantics: `docs/guides/dsr.md`
|
|
- PII field tagging (`custom.pii`): `docs/compliance/README.md`
|
|
- Retention policy (`custom.retention`): `docs/compliance/retention-policy.example.yml`
|
|
- Glossary: `SubjectLink`, `DeletionCertificate` in `docs/glossary.md`
|