# Users can permanently delete their account and all associated PII

- **Pattern:** `ab-000844` (`data-protection.user-rights-access.account-deletion`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000844
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000844)

## Why it matters

GDPR Art. 17 (right to erasure), CCPA §1798.105, and LGPD Art. 18(VI) all require that users can request permanent deletion of their personal data. A soft-delete flag with no automated purge fulfills none of these obligations — the PII still exists in the database and in backups. ISO-27001:2022 A.8.10 requires documented and implemented media disposal and data deletion controls. The practical risk: a soft-deleted user's email and personal data sitting in your database is indistinguishable from active data to a database attacker or a regulatory auditor. Orphaned records in orders, activity logs, and uploaded files that reference a deleted user's ID also constitute retained PII that should have been erased.

## Severity rationale

Low because the right to deletion is user-triggered rather than an automatic systemic exposure, but non-compliance with a submitted deletion request is a direct Art. 17 violation.

## Remediation

Implement a two-phase deletion: immediate soft-delete that revokes access, followed by a cron-driven permanent purge after the grace period.

```ts
// Phase 1: app/api/user/delete/route.ts — immediate soft delete
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: { deletedAt: new Date(), email: null, name: 'Deleted User' }
  })
  return Response.json({ ok: true })
}

// Phase 2: app/api/cron/purge-deleted/route.ts — permanent purge after 30 days
export async function GET() {
  const cutoff = new Date()
  cutoff.setDate(cutoff.getDate() - 30)
  const toDelete = await db.user.findMany({ where: { deletedAt: { lt: cutoff } }, select: { id: true } })

  for (const user of toDelete) {
    await db.$transaction([
      db.activityLog.deleteMany({ where: { userId: user.id } }),
      db.order.deleteMany({ where: { userId: user.id } }),
      db.user.delete({ where: { id: user.id } }),
    ])
  }
  return Response.json({ purged: toDelete.length })
}
```

Add a "Delete Account" button in user settings with a confirmation modal that states what will be deleted and when the purge completes. Schedule the purge cron in `vercel.json` or your equivalent scheduler.

## Detection

- **ID:** `account-deletion`
- **Severity:** `low`
- **What to look for:** Enumerate every relevant item. Find the account deletion feature, typically in user settings under "Account," "Privacy," or "Danger Zone." Verify the implementation: does deletion hard-delete user data, or only soft-delete (set a `deleted_at` flag)? If soft-delete, check whether there is an automated process to permanently purge data after a grace period (typically 30 days). Does deletion cascade to related tables (orders, activity logs, uploaded files, messages)? Is there a confirmation step warning the user that deletion is permanent? Are there orphaned records left behind (e.g., analytics events with the user's ID that are never cleaned up)?
- **Pass criteria:** At least 1 of the following conditions is met. Users can delete their account from a settings page. A confirmation step warns that deletion is irreversible. After deletion (or after a documented grace period), all associated PII is permanently removed from the database. Related records in all tables are deleted or anonymized. Uploaded files in storage are removed.
- **Fail criteria:** No account deletion feature exists. Deletion only sets a `deleted_at` flag with no automated permanent deletion. Related records (orders, logs) are not cleaned up, leaving orphaned PII.
- **Skip (N/A) when:** Application has no user accounts.
- **Detail on fail:** Example: `"No account deletion option found in user settings."` or `"Delete account sets deleted_at flag but no cron job exists to permanently purge data after the 30-day grace period."` or `"Deletion deletes the user row but orphans orders and activity logs containing PII."`.
- **Remediation:** Implement a two-phase deletion with cleanup:

  ```ts
  // Phase 1: Immediate soft delete (user-triggered)
  // app/api/user/delete/route.ts
  export async function DELETE() {
    const session = await getServerSession()
    if (!session?.user?.id) return new Response('Unauthorized', { status: 401 })

    // Mark for deletion — user loses access immediately
    await db.user.update({
      where: { id: session.user.id },
      data: { deletedAt: new Date(), email: null, name: 'Deleted User' }
    })
    // Revoke session
    return Response.json({ ok: true })
  }

  // Phase 2: Permanent purge (run via cron after 30-day grace period)
  // app/api/cron/purge-deleted-users/route.ts
  export async function GET() {
    const cutoff = new Date()
    cutoff.setDate(cutoff.getDate() - 30)

    const toDelete = await db.user.findMany({
      where: { deletedAt: { lt: cutoff } },
      select: { id: true }
    })

    for (const user of toDelete) {
      // Cascade: delete all associated data
      await db.$transaction([
        db.activityLog.deleteMany({ where: { userId: user.id } }),
        db.order.deleteMany({ where: { userId: user.id } }),
        db.userPreferences.deleteMany({ where: { userId: user.id } }),
        db.user.delete({ where: { id: user.id } }),
      ])
      // Also delete from file storage (S3, Supabase Storage, etc.)
    }

    return Response.json({ purged: toDelete.length })
  }
  ```

  Add a "Delete Account" button in settings with a confirmation modal that explains the 30-day grace period and what will be deleted.

---

## External references

- gdpr Art. 17
- ccpa §1798.105
- lgpd Art. 18(VI)
- iso-27001 A.8.10

Taxons: privacy-consent, data-integrity

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