Users can request permanent deletion of their personal data and account
Why it matters
GDPR Art. 17 — the 'right to be forgotten' — requires permanent deletion of personal data when the user requests it and no overriding legal basis for retention exists. A soft-delete that sets a deleted_at flag without a subsequent purge job leaves PII sitting indefinitely in your database, which is a direct Art. 17 violation every day after the grace period expires. Cascade failures are equally common: deleting the user row while leaving activity logs, uploaded files, and messages orphaned with the original user_id and any associated PII is not erasure. Regulators do not accept 'the data is inaccessible through the UI' as equivalent to deletion.
Severity rationale
High because failing to implement actual erasure — not just soft-delete — keeps PII in your database beyond the user's exercise of their Art. 17 right, constituting ongoing unlawful retention under Art. 5(1)(e).
Remediation
Implement two-phase deletion: immediate access revocation followed by a cron-triggered full purge after a documented grace period (typically 14–30 days).
// Phase 1: app/api/user/delete/route.ts — revoke access immediately
export async function DELETE() {
const session = await getServerSession()
if (!session?.user?.id) return new Response('Unauthorized', { status: 401 })
await db.user.update({
where: { id: session.user.id },
data: { email: `deleted-${session.user.id}@purged.invalid`, name: 'Deleted User', deletedAt: new Date() }
})
await db.session.deleteMany({ where: { userId: session.user.id } })
return Response.json({ ok: true })
}
// Phase 2: app/api/cron/purge-deleted-users/route.ts — run after grace period
export async function GET() {
const cutoff = new Date(Date.now() - 30 * 86400_000)
const toDelete = await db.user.findMany({ where: { deletedAt: { lt: cutoff } }, select: { id: true } })
for (const { id } of toDelete) {
await db.$transaction([
db.activityLog.deleteMany({ where: { userId: id } }),
db.consentRecord.deleteMany({ where: { userId: id } }),
db.order.updateMany({ where: { userId: id }, data: { userId: null, customerEmail: 'purged@deleted.invalid' } }),
db.user.delete({ where: { id } }),
])
}
return Response.json({ purged: toDelete.length })
}
Also delete uploaded files from object storage inside the purge loop. Document any data retained beyond the grace period (e.g., financial records for 7 years under tax law) with its Art. 6(1)(c) legal obligation basis.
DetectionPro
The detection method for this pattern is part of AuditBuffet Pro.
The full method (what to look for, pass and fail conditions, and how results get reported) ships inside the GDPR Readiness Audit. The pattern definition, severity rationale, and remediation remain openly licensed under CC-BY-4.0.
External references
- gdpr · Art. 17 — Right to erasure ('right to be forgotten')
- gdpr · Art. 5(1)(e) — Storage limitation principle
- cwe · CWE-1272 — Sensitive resource not removed after period of prescribed use
- soc2:2017 · C1.2 — Disposal of confidential information
Taxons
History
- 2026-04-18·v1.0.0·Initial import from gdpr-readiness·automated
- 2026-07-06·v1.0.1·Add SOC 2 TSC mapping (C1.2) from the verified compliance crosswalk register (docs/compliance/).·by soc2-mapping-enrichment-ship-2