Supabase app hacked: what to do, in order
If your Supabase app was hacked, close the entry point, rotate every key the attacker could read, and then read the logs. Work in that order. A new key does not help if the table is still open. A closed table does not help if the attacker still holds a service_role key.
This guide covers the first hours of an incident. It shows how to confirm the attack, how to stop it, and how to check the damage. For the full RLS design, read the Supabase RLS guide.
What does “my Supabase app was hacked” mean?
It means that someone read, changed, or deleted data in your project without your permission. In a Supabase project, the attacker nearly always used one of five entry points.
| Entry point | How the attacker uses it |
|---|---|
| Table without Row Level Security (RLS) | The attacker sends requests to the public API with the public key. The API returns or changes every row. |
Leaked service_role or secret key |
The key skips RLS. The attacker has full access to all data. |
| Weak or stolen dashboard login | The attacker opens your project as an admin, or removes data in the SQL editor. |
| Leaked database password | The attacker connects to the Postgres database directly. |
| Weak app logic | The attacker changes a role field, or calls a function that has no check. |
The public API of Supabase is not a flaw by itself. The public anon key (or the publishable key) is made to be in browser code. RLS is the control that protects the rows. A table in the public schema without RLS is open to every visitor. The Supabase RLS documentation says that such a table is “readable and writable by any role with a grant on it.”
AI coding tools often create this state. The tool makes a table, and the app works, so nobody checks the policies. If you want to know why the public key is safe only with RLS, read the Supabase anon key guide.
How can I confirm that someone hacked my app?
Look for the signs below. One sign is a reason to check. Two signs are a reason to start the fix.
- Rows exist that you did not create, or rows are missing.
- New users appear that you do not know. Some users have an admin role.
- Your users report spam, password reset emails, or changes in their accounts.
- The API request count rises sharply with no change in your real traffic.
- The Security Advisor shows an error for a public table without RLS.
- You received a message from a person who says that they have your data.
Then run two quick tests.
- Test the public API. Use the public key from your own frontend code. Send a read request to a table that holds user data.
curl "https://YOUR_PROJECT_REF.supabase.co/rest/v1/profiles?select=*&limit=5" \
-H "apikey: YOUR_PUBLIC_KEY"
If the response shows rows from other users, the table is open. Replace profiles with each table that holds private data. Use a new private browser window or a terminal. Do not use a signed-in session, because a signed-in session can pass a policy that blocks the public.
- Run the Security Advisor. In the Dashboard, open the Security Advisor. The Supabase advisors documentation lists the lint
0013_rls_disabled_in_public. Its text says that anyone with your project URL can read, edit, and delete all data in the table.
Warning: Do not delete data to “clean up” before you read the logs. Deleted rows and changed rows are evidence. You need them to learn what the attacker did.
What do I do first? Fix it now, in order of urgency
Do the steps in this order. Steps 1 to 3 close the entry points. Steps 4 to 7 remove the access that the attacker may still hold.
Caution: Some steps break your app for a short time. This is the correct trade. An app that is down for an hour costs less than an attacker who stays inside.
- Secure your Supabase account. Change the password of your Supabase login. Turn on multi-factor authentication. Open the member list of your organization and remove every person that you do not know. Do the same for your GitHub account if it is linked to the project.
- Turn on RLS for every table in the
publicschema. Run this command for each table:
alter table public.profiles enable row level security;
After you turn on RLS, the public API returns no rows until a policy allows them. This closes the table at once. Then add a policy for each table so that your real users can work again:
create policy "own rows select" on public.profiles
for select to authenticated
using ((select auth.uid()) = user_id);
The Supabase RLS documentation also shows how to revoke grants from the anon role on tables that the public API does not need to reach.
- Find the root cause before you rotate. The Supabase key documentation says to fix the root cause of a leak first. If the key sits in your frontend bundle or in a public repository, a new key leaks in the same way. Search your repository and your live bundle for the key. The service_role key guide shows where these keys leak.
- Create a new secret key. Open Settings > API Keys in the Dashboard. Create a new secret key. It starts with
sb_secret_. Store it only in server-side environment variables. - Replace the old key everywhere. Update your server code, your Edge Function secrets, your CI jobs, and your host settings. Then deploy. Confirm that no component uses the old key.
- Retire the old key. The documentation says to delete a compromised secret key, and that this cannot be undone. For a legacy
anonorservice_rolekey, you deactivate it in the same section. A deactivated legacy key can be turned on again if you find a client that you missed. - Reset the database password. Open the Database Settings page and set a new password for the
postgresrole. Services that Supabase manages update by themselves. Any external service with a stored password needs a manual update.
Then handle the sessions and the other secrets.
- Rotate the JWT signing keys if the attacker could have forged tokens. Open the JWT signing keys page. Use Migrate JWT secret if the project still uses the legacy secret. Then use Rotate keys. The documentation says that tokens signed with the old key stay valid until they expire, so no user is signed out. In an active incident, you can revoke the old key at once. To revoke the legacy JWT secret, you must first disable the
anonandservice_rolekeys. - Sign out the users that you do not trust. The signOut reference shows the scopes
global,local, andothers. The defaultglobalscope ends all sessions of that user. The same page says that no method revokes an access token before it expires. A short JWT expiry limits that window. Lower the expiry in the Auth settings of your project. - Rotate every other secret that the project holds. Change the keys for Stripe, OpenAI, email providers, and webhooks. Set them again with
supabase secrets set NAME=valuefor Edge Functions. An attacker with database access can read any secret that you stored in a table. - Remove the attacker’s access inside the app. Delete or disable unknown users. Remove admin roles that you did not grant. Remove functions, triggers, and policies that you did not create. Keep a copy of each object before you remove it.
- Test again from outside. Repeat the
curltest from the confirm section. Check that a signed-in user cannot read the rows of another user.
If a step looks too big for the time that you have, do steps 1, 2, and 4 to 6 first. They close the most likely entry points.
How do I check what the attacker did?
Read the logs for the period before you noticed the attack. The attack often started days earlier. Work from the first sign backward.
The Supabase logs documentation says that Logs covers API Gateway, Postgres, Auth, Storage, PostgREST, Edge Function, Realtime, and pooler events. You open it at https://supabase.com/dashboard/project/_/logs. Replace _ with your project reference. The same page says that retention depends on your plan, so read the logs first. Old entries can expire.
Use this list:
- API Gateway logs. Look for requests to your tables from unknown IP addresses. Look for a high volume of
GETrequests to one table. A bulk read is the usual sign of a data copy. Look forPOST,PATCH, andDELETErequests to tables that your app does not change. - Auth logs. Look for new sign-ups, many failed sign-ins, and password reset requests that you did not start.
- Postgres logs. Look for queries that you did not write. The Supabase roles documentation states that Dashboard queries run as
postgreswith a-- source: dashboardcomment. Use that comment to find work done in the Dashboard. - Edge Function logs. Look for calls to functions that have no auth check.
- Storage logs. Look for downloads and uploads of files that the public should not read.
Then compare the data. Sort each important table by created_at and updated_at. Look for rows from the time of the attack. Check the users table for new accounts and for changed roles. Check payment and order tables for changes that you cannot explain.
If you have daily backups or Point-in-Time Recovery, you can compare a backup with the live data. The Supabase backup documentation says that the Duplicate Project feature restores a backup into a new project. Use the new project as a read-only copy for the comparison. Do not restore over your live project before you finish the investigation. A restore makes the project inaccessible during the process, and it can remove evidence.
Note: Logs show requests. They do not prove which data left the system. If an open table was readable, assume that the attacker copied all its rows.
Write a short timeline: the first sign, the entry point, the time of each fix, and the tables that the attacker could read. You need it for any user notice.
How do I stop it from happening again?
Use these controls. Each one closes one of the entry points from the start of this guide.
- Keep RLS on for every table in an exposed schema. Create each new table with RLS and at least one policy in the same migration. Run the Security Advisor after each release.
- Keep secret keys on the server. A
service_rolekey or asb_secret_key must never appear in browser code. Do not use aVITE_orNEXT_PUBLIC_prefix for any secret. The environment variables guide explains the rules. - Make a policy for each role that writes data. Use
with checkon insert and update policies, so that a user cannot write a row for another user or set their own role. - Protect the dashboard. Use MFA for every member. Give each member the lowest role that they need. Remove members when they leave.
- Turn on backups. Daily backups cover Pro, Team, and Enterprise projects. The documentation does not list automatic backups for free projects. On the free plan, export the data on a schedule with
supabase db dump, and keep the copy outside the project. - Add a check for the public API to your release process. Run the
curltest with the public key on each table after each deploy. - Scan the deployed app. A scan from outside tests what an attacker can reach. It finds open tables and keys in the bundle that the source code review can miss.
When should I get help?
Get help in these cases:
- The attacker still has access after you complete the steps. Signs are new rows, new users, or new requests after the rotation.
- The attacker could read personal data, payment data, or health data. You may have legal duties to report the breach. Ask a lawyer or a privacy officer.
- You cannot find the entry point. A security engineer can review the project, the policies, and the logs.
- The logs expired before you read them, and you cannot say what the attacker took.
- You find a database function, a trigger, or an Edge Function that you did not write. These can give the attacker a way to return.
If the incident is large, create a new project, move the data that you trust, and set new keys from the start. Do this only after you have copied the evidence from the old project.
Common questions
Does Supabase get hacked, or is my app the problem?
Can I pause the project to stop the attack?
Will rotating the keys sign out my users?
How do I know if the attacker copied my data?
Do I need to tell my users?
Find the open door
A VibeEval scan tests your deployed app the way an outsider does, and it reports tables that the public API reads and secret keys that the browser downloads.
14-day free trial · No credit card · Cancel anytime