Safeguard
Data Security

Soft-Deleted Records Are Still Reachable

A soft delete sets a flag and leaves the row in place, which means every single query, export, search index, and cache that touches that table has to remember to exclude it. One that does not is a path where a user's deletion was never actually honored.

Marcus Chen
Application Security Engineer
7 min read

A user deletes their account. A support agent deletes a ticket. An admin deletes a document. In every case, the record does not actually leave the database. A flag gets set, deleted_at gets a timestamp, and the row stays exactly where it was, because rebuilding every downstream process to handle true removal is expensive and soft deletes let you recover from a mistake.

This is a reasonable trade-off, and it works, as long as every single path that reads data actually respects the flag. The failure mode is not soft delete itself. It is the one query, export, or cache layer that was never updated to filter it out.

Why the flag does not travel with the data

A deleted_at IS NULL clause is not a property of the row. It is a condition that has to be repeated in every query that touches the table, by every developer, in every service, for the entire lifetime of the codebase. The moment a new query is written, a new report is added, or a caching layer is introduced in front of the database, someone has to remember to add that clause again, and nothing about the schema itself enforces it.

This is structurally the same problem as any other filter that depends on being remembered rather than being guaranteed: a permission check that has to be added to every endpoint individually, a tenant scope that has to be applied to every query by hand. The failure is not a single mistake. It is the absence of anything that would have caught the one place the pattern was not followed.

Where soft-deleted data actually resurfaces

Pagination and cursor-based listing endpoints, where a query built before the soft-delete flag existed, or built by someone unaware of the convention used elsewhere in the codebase, lists records without the exclusion clause. The deleted record appears in a paginated list days or months after a user was told it was removed.

Search indexes and caches that are updated on write but not on delete. A record indexed into a search service when created, and never explicitly removed from that index when soft-deleted, continues to appear in search results indefinitely, because the index has no concept of the flag unless the deletion path was specifically built to propagate to it.

Bulk exports, reports, and analytics pipelines, which frequently query the underlying tables directly rather than through the same data-access layer the main application uses, and which were built by a different team, at a different time, with no reason to know about a soft-delete convention introduced elsewhere. An export feature is a common place for this to surface because it is often treated as a lower-priority, less-reviewed code path than the primary application.

Foreign key references and joins from other tables, where a soft-deleted parent record is still referenced by a child record, and a join that fetches the child along with its related parent returns the parent's data regardless of its deletion state, because the join condition was never written to check it.

API responses that expose an internal identifier, where the identifier for a soft-deleted record remains valid and returns data through a direct lookup endpoint even though the record no longer appears in any listing, because the listing endpoint respects the flag and the individual-record endpoint does not.

Why this becomes a compliance and trust problem, not just a bug

A user who deletes their data, particularly under a regulatory right to erasure, has a reasonable expectation that the data is actually gone, or at minimum inaccessible. A soft delete that is bypassed by even one query path means that expectation is false, and it is false in a way that is invisible from the outside: the user sees their record disappear from every interface they interact with, while the data remains fully queryable through a path they never see.

This is a particularly uncomfortable category of defect to discover during an audit or a legal request, because the natural response, "we deleted it," is not actually true, and demonstrating that it is true requires checking every single code path that touches the table, not just the one the user interacted with.

What actually closes the gap

Enforce the filter at the lowest possible layer, not at the query-writing layer. A database view that already excludes soft-deleted rows, a default scope applied automatically by the data-access framework, or a row-level security policy that filters by the flag removes the dependency on every individual query author remembering to add the clause. The fewer places the exclusion has to be repeated by hand, the fewer places it can be forgotten.

Treat any query that bypasses the default scope as something that needs explicit justification and review, the same way a raw SQL query bypassing an ORM's tenant isolation would. An admin interface that legitimately needs to see soft-deleted records for recovery purposes is a reasonable exception; it should be marked as one, not indistinguishable from every other query in the codebase.

Audit every downstream consumer of the primary tables separately from the main application, specifically search indexes, caches, export jobs, and analytics pipelines, because these are built and maintained on different schedules by different people, and are the most likely places for the soft-delete convention to have been missed entirely rather than incorrectly implemented.

For data subject to an actual erasure obligation, do not rely on soft delete alone. Build a separate, scheduled hard-delete or field-level scrubbing process that actually removes or irreversibly anonymizes the sensitive fields after a defined retention window, so that the soft-delete flag is a recovery mechanism for accidental deletion, not the final answer to a request that legally requires the data to be gone.

Check yours

Pick a record type your product allows a user to delete, and after soft-deleting one, check three things a normal user would never see: whether it appears in a bulk export, whether it is still findable through search, and whether its direct API endpoint still returns data by identifier. Any yes is a path where a user's deletion was not actually honored, and it is worth finding through this exercise rather than through a data subject access request or an audit.

The concession

Building every single downstream system, from day one, to correctly and completely respect a soft-delete flag is more coordination than most engineering organizations achieve in practice, and soft delete's core value, the ability to recover from an accidental removal, genuinely requires the data to still exist somewhere for a period of time. Hard-deleting immediately on every action is not a realistic default either.

The proportionate approach is layered: default-scope enforcement everywhere it is technically possible, explicit and reviewed exceptions where it is not, and a genuine hard-delete or scrub process specifically for anything a legal or contractual obligation actually requires to be gone, rather than trusting the soft-delete flag to satisfy that obligation by itself.

The implication

A soft-deleted record is not gone. It is a row with a flag, sitting in the same table, reachable by any query that was written, updated, or forgotten without that flag in mind, and the number of such queries only grows as a codebase ages and more systems come to depend on the same underlying tables.

Check what actually happens to a record after your product tells a user it has been deleted. The gap between what the interface shows and what a direct query, export, or search index still returns is exactly where a soft delete quietly stops meaning what everyone assumes it means.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.