# Written retention schedule for each data type; automated deletion confirmed

- **Pattern:** `ab-000838` (`data-protection.storage-retention.retention-schedule-documented`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000838
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000838)

## Why it matters

GDPR Art. 5(1)(e) requires data to be kept in identifiable form no longer than necessary — "storage limitation" is a core data protection principle, not an optional enhancement. Art. 13(2)(a) requires retention periods to be disclosed in the privacy policy, creating a legal promise you must technically enforce. CCPA §1798.105 gives California residents the right to demand deletion, which requires that you have the infrastructure to actually delete. ISO-27001:2022 A.8.10 requires media disposal and data deletion controls. An undocumented or unenforced retention schedule is also a significant incident amplifier: data you promised to delete but didn't can appear in a breach years after the user expected it was gone.

## Severity rationale

High because failing to delete data per the documented schedule constitutes both a broken legal promise and a GDPR Art. 5(1)(e) violation, and undeleted data expands the blast radius of any future breach.

## Remediation

Document retention periods in your privacy policy and enforce them with an automated cron job. Align the cron schedule with what the policy promises.

```ts
// app/api/cron/cleanup/route.ts
// Schedule via vercel.json: { "crons": [{ "path": "/api/cron/cleanup", "schedule": "0 2 * * *" }] }
export async function GET() {
  const logCutoff = new Date()
  logCutoff.setDate(logCutoff.getDate() - 90)

  await db.auditLog.deleteMany({ where: { createdAt: { lt: logCutoff } } })

  const graceCutoff = new Date()
  graceCutoff.setDate(graceCutoff.getDate() - 30)

  await db.user.deleteMany({ where: { deletedAt: { lt: graceCutoff } } })

  return Response.json({ ok: true })
}
```

For each data type in your policy, implement the matching cleanup: analytics events at 26 months, server logs at 90 days, deleted-account data after the grace period. Verify cron jobs are actually running by checking deployment logs monthly.

## Detection

- **ID:** `retention-schedule-documented`
- **Severity:** `high`
- **What to look for:** Enumerate every relevant item. Look for a data retention schedule — in the privacy policy (must state retention periods), a separate RETENTION_POLICY.md, or database schema comments. Check for automated deletion processes: cron jobs, database lifecycle policies, Supabase scheduled functions, Vercel cron, or similar. Search for delete or cleanup scripts that reference time-based conditions (e.g., `WHERE created_at < NOW() - INTERVAL '90 days'`). Check whether test or log data has a defined cleanup schedule. Verify the schedule aligns with what the privacy policy promises.
- **Pass criteria:** At least 1 of the following conditions is met. A documented retention schedule exists specifying how long each data type is retained (e.g., account data until deletion, analytics 26 months, server logs 90 days, inactive accounts 2 years). Automated deletion processes exist that enforce these schedules, and there is evidence they run (scheduler config, test, or deployment history).
- **Fail criteria:** No retention schedule documented. Data is retained indefinitely with no deletion logic. Privacy policy mentions retention periods but no automated enforcement exists.
- **Skip (N/A) when:** Application collects no personal data that persists beyond a user session.
- **Detail on fail:** Example: `"No retention schedule found. No automated deletion scripts or cron jobs in codebase. User data appears to be retained indefinitely."` or `"Privacy policy states 'data deleted after 30 days of account deletion' but no corresponding cron job or trigger found."`.
- **Remediation:** Document and automate retention:

  ```ts
  // Retention policy (document in privacy policy + implement here)
  //
  // Account data:        Until account deletion (user-triggered)
  // Analytics events:    26 months from collection
  // Server/audit logs:   90 days
  // Inactive accounts:   Warn at 18 months, delete at 24 months
  // Deleted user data:   30-day grace period, then permanent deletion

  // Example: Vercel cron (vercel.json)
  // { "crons": [{ "path": "/api/cron/cleanup", "schedule": "0 2 * * *" }] }

  // app/api/cron/cleanup/route.ts
  export async function GET() {
    const cutoff = new Date()
    cutoff.setDate(cutoff.getDate() - 90)

    // Delete old audit logs
    await db.auditLog.deleteMany({
      where: { createdAt: { lt: cutoff } }
    })

    // Permanently delete accounts marked for deletion > 30 days ago
    const graceCutoff = new Date()
    graceCutoff.setDate(graceCutoff.getDate() - 30)
    await db.user.deleteMany({
      where: { deletedAt: { lt: graceCutoff } }
    })

    return Response.json({ ok: true })
  }
  ```

---

## External references

- gdpr Art. 5(1)(e)
- gdpr Art. 13(2)(a)
- iso-27001 A.8.10
- ccpa §1798.105

Taxons: privacy-consent, data-integrity

HTML version: https://auditbuffet.com/patterns/ab-000838
