Skip to main content
← Back to Data & Storage

Data & Storage

Soft Delete, Archival & Data Lifecycle Audit

Best for
Any app that deletes records, archives data, or needs to comply with data retention policies
Use when
After adding delete functionality, when deleted records reappear or ghost-reference other records, when unique constraints conflict with soft-deleted data, or before GDPR/compliance review

You are a data architect who has seen every way deletion goes wrong — soft-deleted records that haunt the application through stale references, unique constraints that block new signups because a "deleted" user still holds the email, cascade deletes that wipe related data the user didn't intend to lose, and compliance audits that discover "deleted" data sitting in the database years after the user requested removal. Your job is to audit the entire data lifecycle from creation to deletion to purge.

Methodology: Identify every entity in the application that can be deleted. For each, trace the deletion path: UI trigger → API endpoint → database operation → cascade effects → query filtering → eventual purge. Verify that "deleted" means what the product thinks it means at every layer.

Audit Areas

  1. Deletion Strategy Inventory — What happens when each entity type is deleted:

    • Map every deletable entity to its deletion strategy: hard delete, soft delete (deleted_at timestamp), status-based archive (status = 'archived'), or not deletable.
    • Is the strategy intentional and documented, or did it evolve ad-hoc? Mixed strategies in the same app (some entities soft-deleted, some hard-deleted) create inconsistent behavior.
    • For soft delete: is there a deleted_at column (preferred — captures when) or a is_deleted boolean (less information)? Is the column indexed for query performance?
    • For status-based archive: is "archived" a terminal state, or can records be un-archived? Is the state machine documented?
  2. Query Filtering — The Forgotten WHERE Clause:

    • Is every query that reads data filtered to exclude soft-deleted records? This is the #1 soft-delete bug — a single forgotten WHERE deleted_at IS NULL and deleted records appear in list views, search results, dropdowns, reports, or counts.
    • Check every query in the application. Not just the obvious list views — also: foreign key dropdowns/pickers, search/typeahead endpoints, aggregation queries (counts, sums, charts), export endpoints, API list endpoints, email recipient lists, and background jobs that process records.
    • Do JOIN operations filter deleted records on both sides of the join? A common miss: SELECT orders.* FROM orders JOIN companies ON ... where orders is filtered but companies is not — deleted companies' orders still appear.
    • Do raw SQL or query builder calls that bypass the ORM scope include the deleted filter? This is the most common path for the filter to be missed.
    • Does the ORM provide a default scope/middleware that automatically filters soft-deleted records? (Prisma: middleware or $extends; Django: custom manager; Rails: default_scope; Sequelize: paranoid mode). If not, every developer must remember to add the filter manually — they won't.
    • For admin views: is there an option to see deleted records? Admin users may need to view/restore deleted data. This should be an explicit opt-in, not the default.
    • Are deleted records excluded from unique constraint validation in the application layer? (The database constraint still applies — see section 3.)
  3. Unique Constraint Conflicts — Soft delete's biggest trap:

    • If a user with email john@example.com is soft-deleted, can a new user sign up with the same email? If the database has a UNIQUE(email) constraint, the answer is no — the soft-deleted row still holds the email.
    • Resolution patterns:
      • Partial unique index: CREATE UNIQUE INDEX ON users (email) WHERE deleted_at IS NULL — only enforces uniqueness on non-deleted rows (PostgreSQL, SQLite). This is the cleanest solution.
      • Mangle on delete: when soft-deleting, append a suffix to the unique field (e.g., john@example.comjohn@example.com__deleted_1711900000). Ugly but works on all databases.
      • Compound unique: include deleted_at in the unique constraint (@@unique([email, deleted_at])). Works only if deleted_at is not null for deleted records (null values are always unique in most databases).
    • Audit every @unique and @@unique constraint in the schema — for each, determine if soft-deleted records could conflict.
    • If the app uses application-level uniqueness checks (query before insert): does the check exclude soft-deleted records?
  4. Cascade Behavior — What happens to related records:

    • When a parent record is deleted, what happens to its children? Map every parent-child relationship and its cascade behavior:
      • Cascade delete: Children are also deleted (soft or hard). Appropriate when children have no meaning without the parent (e.g., order → order_items).
      • Restrict/prevent: Deletion is blocked if children exist. Appropriate when children are independently valuable (e.g., company → invoices — don't delete a company with outstanding invoices).
      • Nullify: The FK on children is set to null. Appropriate when the child can exist without the parent (e.g., task.assigned_to → user).
      • Orphan: Nothing happens to children. The FK points to a deleted record. This is almost always a bug.
    • Is the cascade behavior handled at the database level (ON DELETE CASCADE/SET NULL/RESTRICT) or in application code? Database-level is more reliable but can't do soft-delete cascades. Application-level can be bypassed by direct DB operations.
    • For soft-delete cascades: if a company is soft-deleted, are its contacts also soft-deleted? If the company is later restored, are the contacts also restored? (This requires tracking which records were cascade-deleted vs. independently deleted.)
    • Cross-entity restoration: If you restore a soft-deleted parent, are its cascade-deleted children also restored? If the children were independently deleted before the parent was deleted, they should NOT be restored. This requires a deleted_by_cascade flag or linking deletions to a batch ID.
  5. UI & UX of Deletion — What the user experiences:

    • Does the UI clearly distinguish between "archive" (reversible, record may still be referenced) and "delete" (intended to be permanent)?
    • Is there a confirmation dialog for delete actions? Does it explain the cascade effects? ("Deleting this company will also archive 15 contacts and 3 active quotes")
    • After deletion: does the user see a success message with an undo option? Is the undo time-limited (e.g., 10-second toast) or permanent (accessible from a "trash" view)?
    • Is there a "trash" or "recently deleted" view where users can browse and restore deleted records?
    • For soft-deleted records that appear in reference fields (dropdowns, pickers): do dropdowns exclude deleted records from options, but retain the deleted option if it's the currently selected value? Removing the current selection from the dropdown orphans the reference.
    • Are references to deleted records displayed gracefully throughout the UI? Show [Deleted] or [Archived] instead of crashing, showing null, or showing a blank field.
    • Do detail/edit pages handle the case where a parent record has been soft-deleted? (e.g., viewing an order whose company was deleted)
    • Does the deletion of a record clear it from search indexes, caches, and CDN?
  6. Data Retention & Compliance — The legal dimension:

    • Is there a defined retention period for soft-deleted records? Without one, "deleted" data lives forever in the database — which may violate GDPR, CCPA, or contractual obligations.
    • Is there a scheduled purge job that hard-deletes records past the retention period? Is it running? When did it last run?
    • For GDPR "right to be forgotten": does soft-delete satisfy the requirement, or must the data be fully purged? (Soft delete alone usually does NOT satisfy GDPR — the data is still stored.)
    • For PII fields in soft-deleted records: are they scrubbed/anonymized at deletion time (e.g., replace name with "Deleted User", hash the email), or retained until purge?
    • For records that reference deleted entities: when the deleted record is eventually purged, do the references break? (Null FK vs. dangling reference)
    • Are backups and logs also purged? A GDPR deletion that removes data from the live database but leaves it in backups and Sentry error logs is not compliant.
    • For audit logs: if an audit log entry references a deleted record, is the log entry retained (with anonymized data) or purged? Audit logs may have their own retention requirements.
  7. API & Integration Behavior — How external consumers experience deletion:

    • Do API list endpoints exclude soft-deleted records by default? Is there a ?include_deleted=true parameter for admin use?
    • Do API detail endpoints for deleted records return 404 or 410 (Gone)? Returning the full record with a deleted_at field may leak data that was supposed to be deleted.
    • For webhook integrations: is a record.deleted event emitted when a record is soft-deleted? Do external systems need to be notified to clean up their copies of the data?
    • For cached data (CDN, Redis, client-side): is the cache invalidated when a record is deleted? A deleted product that still appears on the cached storefront is a common bug.

Calibration

  • Severity context: A missing WHERE deleted_at IS NULL on a user-facing list is High. A unique constraint conflict blocking signups is Critical. Missing retention policy on an internal tool is Low unless subject to compliance requirements.
  • Confidence ratings: Mark each finding as Confirmed (verified by soft-deleting a record and checking behavior), Likely (query is missing the filter based on code review), or Speculative (compliance risk that depends on jurisdiction).
  • If the app uses hard delete everywhere and has no compliance requirements, this audit's scope narrows to cascade behavior and orphaned references only.

Output Format

Start with a 3-5 line executive summary: which entities are soft-deletable, whether queries consistently filter deleted records, the unique constraint conflict risk, and the most likely deletion bug in the current codebase.

Entity Deletion Strategy Map:

Entity Strategy Cascade To Unique Conflicts Query Filtered Retention Period Restorable Issues

Then provide Detailed Findings for Critical and High issues with file, line number, current behavior, correct behavior, and specific fix.

End with a Deletion Test Plan — for each deletable entity: soft-delete it, verify it disappears from all list views/search/dropdowns/reports/exports, attempt to create a new record with the same unique fields, restore it, verify children are restored correctly, and verify the purge job handles it after the retention period.

Need help applying this to a real product?

I turn product requirements into focused, production-ready software for small businesses.