Files
agentic-dev/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

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`