---
id: 04-retention-purge-job
epic: compliance-manifests-pii-retention-subprocessors
title: Background retention purge job in core-shared
type: technical-story
status: done
feature: core-shared
depends-on: [01-pii-retention-type-primitives]
blocks: []
created: 2026-05-18T17:52:09Z
updated: 2026-05-18T18:58:23.920Z
---

## Goal

Add `core-shared/payload/retention-purge/retention-purge.job.ts` — a module that walks every Payload collection's `custom.retention.purgeSchedule`, registers a per-collection scheduled job via `IJobQueue`, queries rows whose active-retention period has elapsed, then either pseudonymizes or hard-deletes each row while emitting one `IAuditLog.record(...)` entry per row. Optional `auditLog` is skipped gracefully when `core-audit` is not wired.

## Why

Retention without automated purge is a compliance statement with no enforcement. The job makes `custom.retention` actionable: declarations in code become real deletes on schedule.

## Done when

- `packages/core-shared/src/payload/retention-purge/retention-purge.job.ts` exists; receives `IJobQueue` + `SanitizedConfig` (+ optional `IAuditLog`) via constructor/factory.
- At registration time, one scheduled job is created per collection that declares `custom.retention.purgeSchedule`.
- Job body queries by `createdAt` (trigger `from-creation`) or `updatedAt` (trigger `from-last-access`), applies `pseudonymize` (NULL PII fields) or `hard-delete` (Payload cascade delete) per `postDeletion.action`.
- Each processed row emits `auditLog.record({ action: "DELETE", subject: row.id, actor: "system", reason: "retention-policy" })`; when `auditLog` is undefined the emission is silently skipped.
- Unit tests with an in-memory Payload mock cover: schedule registration per collection, row matching per trigger type, audit emission, pseudonymize vs hard-delete branches, and graceful auditLog skip.
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.

## In scope

- `packages/core-shared/src/payload/retention-purge/retention-purge.job.ts` + sibling unit test.
- Uses existing `IJobQueue` from `core-shared/jobs` and `IAuditLog` from `core-audit` (optional injection).

## Out of scope

- App-side boot wiring — the module is the deliverable; consumers integrate it into their `bindAll()`.
- `lastAccessedAt` field hook for true "from-last-access" tracking (deferred; see PRD Q2).
- Advisory lock for concurrent purge guard (deferred; see PRD Q4).

## Tasks

- [x] Add `retention-purge.job.ts` + unit tests to `packages/core-shared/src/payload/retention-purge/` — walks `custom.retention.purgeSchedule` per collection, registers scheduled jobs via `IJobQueue`, executes pseudonymize or hard-delete per `postDeletion.action`, emits optional audit entry per row, unit tests cover all branches including graceful auditLog skip — all gates pass on this commit.
