chore(template): clean-slate template snapshot from bb4a0c7
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
This commit is contained in:
170
docs/compliance/subject-linkage.example.md
Normal file
170
docs/compliance/subject-linkage.example.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# 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`
|
||||
Reference in New Issue
Block a user