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