Curated, product-agnostic snapshot of the post-story-04 tree: demo content deleted, auth-only reference feature, web-next shell, all gates green. Product-specific docs, ADRs 027-029, PRDs/epics/archive, editor library traces, and product naming are curated out; generic template repairs (coverage provider devDeps, root test:coverage script, live lint fixes, root-only release-please) are kept. See TEMPLATE.md for provenance, curation list, and usage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
5.6 KiB
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:
- Which rows to include in an Art. 15 export (
dsr.export). - Which rows to delete or redact in an Art. 17 erasure (
dsr.delete).
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.
// 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 indata["support-tickets"].asSelf(filtered to exportable PII fields:body).assignedTo === alice→ ticket row IDs + link coordinates appear indata["support-tickets"].asReference.
Art. 17 soft delete for user alice:
- Rows where
submittedBy === alice→bodyfield NULLed (pseudonymized, perpostDeletion.action = "pseudonymize"). - Rows where
assignedTo === alice→assignedTofield NULLed; row is otherwise untouched.
Art. 17 cascade-hard delete for user alice (admin only):
- Rows where
submittedBy === aliceandpostDeletion.actionresolves to"hard-delete"→ rows deleted entirely. - Rows where
assignedTo === alice→assignedTofield 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:
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:
// 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:
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,DeletionCertificateindocs/glossary.md