Files
agentic-dev-template/docs/compliance/subject-linkage.example.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

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.


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).
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 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 === alicebody field NULLed (pseudonymized, per postDeletion.action = "pseudonymize").
  • Rows where assignedTo === aliceassignedTo 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 === aliceassignedTo 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:

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, DeletionCertificate in docs/glossary.md