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.
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.
Document retention periods in your privacy policy and enforce them with an automated cron job. Align the cron schedule with what the policy promises.
// 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.
ID: data-protection.storage-retention.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:
// 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 })
}