Deletes with no soft delete or audit trail
What it looks like
// "Delete project" button handler
const { error } = await supabase
.from('projects')
.delete()
.eq('id', projectId);
// no deleted_at column, no archive table, no log of who did itWhat happens
One wrong click, a bug in a filter, or an abused account removes customer data for good, and nobody can say afterwards what was lost or who removed it.
Where it shows up
Descriptive, not a verdict: builders and stacks whose defaults make this reachable, or that a public incident names. Each hub states the tool's own security features.
Check it yourself
Uses your builder's dashboard and your browser. No tools to install.
- In a test account, create a record in the app, then delete it using the app's own button.
- Open the table in the Supabase Table Editor (or the Firebase console) and search for that record by name or id.
- Look for a
deleted_atcolumn, an archive table, or an audit table with a row describing the delete. - Bad result: the row is gone, no table records who deleted it or when, and the only recovery path would be a full database restore.
What we check
We trace every delete path in the API and the database, including cascade rules (settings that remove linked rows when a parent is deleted), and record which rows disappear together. The report shows which of those deletes can be recovered and which cannot.
Fix direction
Mark rows as deleted with a deleted_at timestamp and filter them out in queries, or move them to an archive table on delete. Add a trigger (a database rule that runs on each change) that writes who, what and when to an audit table. Reserve hard deletes for a scheduled cleanup after a retention period.
Mechanism documented in: Postgres Triggers (verified 2026-09-21).
Related patterns
- No backup that has ever been restoredData integrityHigh
When data is deleted or corrupted, whether by a bug, an agent or a person, there is either nothing to restore or a backup nobody has proven they can actually bring back.
- Migrations that cannot be undoneData integrityMedium
When the change turns out to be wrong, the old column values and deleted rows are already gone, and the only way back is a full restore from backup, if one exists.
- Record ids accepted without an ownership checkData accessCritical
Anyone who changes the id in a URL or request can read, edit or delete another customer's record.
- Row Level Security disabled on a tableData accessCritical
Anyone with the project URL and the public anon key, both of which sit in the app's JavaScript, can read, edit or delete every row in the table.
Questions
what is soft delete in a database
A soft delete marks a row as deleted, usually by setting a deleted_at timestamp, instead of removing it. The app hides those rows in its queries, so users see the same result, but the data stays in the table until a later cleanup. It gives you a window to undo a mistaken delete and a record of what was removed.
how do i add an audit log to supabase
Supabase's Postgres trigger docs show the mechanism: a function that runs after an insert, update or delete and writes the old and new values to a log table. Attach one to each table that holds customer data, recording the row id, the action, the acting user and a timestamp. Keep the log table writable only by the trigger, not by the app's normal role.
how to recover a deleted row in supabase
If the table has no soft-delete marker or audit copy, the row exists only in a backup, if your plan includes one, and getting it back means restoring or querying that backup. Check your project's Backups page for what is available before you need it, and add a deleted_at column so the next delete is reversible in place.
Back to the catalogue, or run the self-check for your stack.