Supabase table data deleted by someone: recover it and stop it
If someone deleted rows from your Supabase table, revoke delete access, restore the rows from a backup into a new project, and read the logs. Do not restore over the live project as your first step. A full restore replaces all newer data and makes the project inaccessible.
This guide covers the case where rows are gone and you do not know why. It shows how to stop more deletes, how to get the rows back, and how to find who deleted them. For a full RLS design, read the Supabase RLS guide.
Why did my Supabase table data disappear?
A SQL delete removes rows for good. Supabase has no recycle bin for table data. Five causes explain most cases of missing rows.
| Cause | What you see |
|---|---|
| Table without RLS, or a delete policy that allows everyone | A stranger deletes rows through the public API with the public key. |
Leaked service_role or secret key |
The attacker skips RLS and deletes any row. |
| Your own code or your AI coding tool | A delete without a where clause, a truncate, a drop table, or a database reset runs on the live project. |
| A team member in the Dashboard | Someone runs a command in the SQL editor or the Table Editor on the wrong project. |
A foreign key with on delete cascade |
You delete one parent row, and the database deletes all the child rows. |
The first two causes are attacks. The last three are mistakes. The fix for the first two is the same as for any open table: RLS and policies. The fix for the last three is a backup and a safer process. You need to know which one you have, so check the cause before you close the incident.
The public anon key is made to be in browser code. RLS is the only control that decides what that key can do. The Supabase RLS documentation says this about a table in an exposed schema without RLS. The table is “readable and writable by any role with a grant on it.” Delete is one of the writes that this allows.
How do I confirm who can delete my rows?
Check the policies, the grants, and the RLS flag. Use the SQL editor in the Dashboard. These queries only read data.
- List the tables without RLS.
select schemaname, tablename
from pg_tables
where schemaname = 'public' and not rowsecurity;
Every table in this list is open to the public API. Delete is possible on all of them.
- List the delete policies.
select tablename, policyname, roles, cmd, qual
from pg_policies
where schemaname = 'public' and cmd in ('DELETE', 'ALL');
Read the qual column. A value of true means that every row can be deleted by the listed roles. If roles includes anon or public, a visitor without an account can delete the rows.
- List the delete grants.
select table_name, grantee
from information_schema.role_table_grants
where table_schema = 'public'
and privilege_type = 'DELETE'
and grantee in ('anon', 'authenticated');
A grant does not mean that a user can delete rows. RLS and policies still apply. A grant on a table without RLS does mean it.
- Check your code. Search the repository for
.delete(),delete from,truncate, anddrop table. Check the migration files and any script that an AI tool wrote. Look at thewhereclause of each statement.
Warning: Do not test a delete on real data. If you want to test the public API, create a test table with a few dummy rows and run the test there.
What do I do first? Stop more deletes
Do these steps in order. They take a few minutes. Close access before you restore, or the same cause can delete the rows again.
Caution: These steps can break parts of your app for a short time. Data loss costs more than a short outage.
- Turn on RLS for each table in the
publicschema.
alter table public.orders enable row level security;
After RLS is on, the public API returns no rows and accepts no changes until a policy allows them. This closes the table at once.
- Remove the delete privilege from the public roles. A policy cannot remove a grant, so revoke the grant itself. The Supabase RLS documentation shows this pattern:
revoke delete on table public.orders from anon, authenticated;
Most apps let users delete only a few kinds of rows. Add the privilege back for those tables only, with a policy that checks ownership.
- Drop the dangerous delete policies. Use the list from the confirm section. Remove each policy with
using (true).
drop policy "anyone can delete" on public.orders;
- Rotate the keys if you suspect a leak. If the logs show deletes from an unknown source and RLS was already on, a secret key may have leaked. Read the service_role key guide and rotate the key. Then replace it in your server code.
- Pause scripts and scheduled jobs that delete data. Check your cron jobs, Edge Functions, and CI tasks. Stop any that you did not plan.
- Save the current state. Export the live database before you change anything else. The Supabase CLI command
supabase db dumpsupports the--data-onlyand-fflags. The CLI reference lists them.
supabase db dump --data-only --db-url "$DB_URL" -f live-before-restore.sql
This file is your safety copy. It also holds evidence for the investigation.
How do I get the deleted rows back?
Use a backup. Which backup you have depends on your plan. The Supabase backup documentation gives these facts.
| Option | Availability | What it gives you |
|---|---|---|
| Daily backups | Pro, Team, and Enterprise. Pro keeps 7 days, Team 14 days, Enterprise up to 30 days. | A full copy of the database from one day. |
| Point-in-Time Recovery (PITR) | An add-on for Pro, Team, and Enterprise. It needs at least a Small compute add-on. | A restore to a chosen second inside your recovery window. |
| Your own dump | Any plan, if you made one before. | The data at the time of the dump. |
The docs list no automatic backups for free projects. For a free project, search for your own copies first. Look for a supabase db dump file or an export from the Table Editor. Check your email for a CSV, and check your analytics and payment tools. Check your Git history for seed files. A copy that is old is still better than no copy.
If you use PITR, choose a restore time just before the first delete. The restore time must be inside the earliest and latest recovery points. Enabling PITR stops the daily backups, because PITR is more exact.
Restore into a new project, not over the live one
The backup page lists these facts about a restore:
- The project is inaccessible during the restore. Larger databases take longer.
- The restore replaces the data in the project. Rows that users added after the backup are lost.
- Daily backups do not store custom role passwords. Reset them after the restore.
- Storage objects are not part of a backup. A restore does not bring back files that you deleted after the backup.
For this reason, use the Duplicate Project feature. It restores a backup into a new project. Your live project stays online while you work.
- Open Database > Backups in the Dashboard of the live project.
- Pick the newest backup that was made before the first delete.
- Restore it into a new project with Duplicate Project.
- Check the table in the new project. Make sure the missing rows exist.
- Dump only that table from the new project.
pg_dump "$NEW_PROJECT_DB_URL" --data-only --table=public.orders \
--column-inserts --on-conflict-do-nothing -f orders-restore.sql
- Load the file into the live project.
psql "$LIVE_DB_URL" -f orders-restore.sql
The --on-conflict-do-nothing option works only with --inserts, --column-inserts, or --rows-per-insert, as the pg_dump documentation states. It adds ON CONFLICT DO NOTHING to each insert. Rows that already exist in the live table stay unchanged, and only the missing rows are added.
Check three things before you load the file. First, the table schema in both projects must match. Second, a restore point that is old holds old versions of rows that users changed later. Third, rows that you deleted on purpose after the backup come back, so remove them again.
Use the full restore over the live project only if the damage is very large. For example, use it if an attacker deleted many tables. Take the safety copy from the save step first.
How do I find out who deleted the data?
Read the logs. The Supabase logs documentation says that Logs covers API Gateway, Postgres, Auth, Storage, PostgREST, Edge Function, Realtime, and pooler events. Retention depends on your plan, so read them early. Open https://supabase.com/dashboard/project/_/logs and replace _ with the project reference.
- Find the time window. Sort the table by
created_ator by the largestid. Find the gap. Use the last backup date and the time of your first report from a user. - Read the API Gateway logs for that window. Search for
DELETErequests to the table. Note the source IP address and the user agent. A browser request from your own domain looks different from a request made withcurlfrom a foreign address. - Read the Postgres logs. The Supabase roles documentation says that Dashboard queries run as
postgreswith a-- source: dashboardcomment. Search for that comment. A match shows work done in the Dashboard by a team member or by an account that took over a login. - Read the Auth logs. Look for sign-ins at the time of the deletes. This shows whether a normal user account made the requests.
- Check your deploy history. Compare the time of each deploy and each migration with the time of the data loss. An AI coding tool can run a migration with a
dropor a reset when you did not expect it. - Check cascades. Read your schema for
on delete cascade. One deleted parent row can remove many child rows. In that case, the cause is one delete, not many.
Note: Logs show requests. They do not always show how many rows a request removed. Use the row counts in the backup to measure the loss.
If the deletes came from a stranger, treat the incident as a break-in. Rotate the keys, check for other changes, and read what the attacker could read. A person who could delete data could also copy it first.
How do I stop it from happening again?
Use these controls. Each one limits a cause from the first table of this guide.
- Make delete rules narrow. A delete policy must check ownership, for example
using ((select auth.uid()) = user_id). Do not useusing (true). - Grant delete only where the app needs it. Revoke
deletefromanonfor all tables. Revoke it fromauthenticatedon tables where users never delete rows. - Use soft delete for important data. Add a
deleted_atcolumn. The app sets the column and hides the row. A cleanup job removes old rows later. A mistake then costs one update, not a restore. - Keep secret keys on the server. Never place a
service_rolekey in browser code. - Turn on backups that fit your risk. Use daily backups at a minimum. Use PITR if you cannot lose more than a few minutes of data. On the free plan, schedule
supabase db dumpand store the file outside the project. - Test a restore. Restore a backup into a new project once, before you need it. Write down the time that it takes.
- Use a staging project. Run migrations and AI-written scripts on a copy first. Never connect a coding tool to the production database with a secret key.
- Review each migration for
drop,truncate, anddelete fromwithoutwhere. Read AI-generated migrations line by line before you apply them. - Scan the deployed app. A scan from outside shows which tables the public key can change or delete. See the Supabase guide for more on the Supabase setup.
When should I get help?
Get help in these cases:
- You have no backup and no other copy of the data. A Supabase support request can tell you what recovery options exist for your plan.
- The deletes continue after you revoke access. Rotate all keys and check for a leaked key or a changed policy.
- You see a function, a trigger, or a scheduled job that you did not create. These can delete data again.
- The deleted data includes records that law or contract requires you to keep, such as invoices.
- The attacker also read personal data. You may have legal duties to report the breach. Ask a lawyer or a privacy officer.
Finish with one more test. Use a signed-in test user and the public key. Try to delete a row that belongs to another user. The API must return no change.
Common questions
Can I recover deleted rows on the Supabase free plan?
Does a deleted row go to a recycle bin?
Will a restore bring back deleted Storage files?
Should I restore over the live project?
How can I tell if my own code deleted the rows?
Test who can delete
A VibeEval scan sends requests to your deployed app as an outsider, and it reports Supabase tables that the public API lets a visitor change or delete.
14-day free trial · No credit card · Cancel anytime