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