16,326 Supabase databases were left open. Here's how to check yours in 10 minutes
UpGuard found thousands of Supabase apps whose tables anyone could read. Six checks, with copy-paste SQL, that tell you whether your database is one of them, and what Supabase's October 30 change does and does not fix.
In September, UpGuard published the largest study yet of exposed Supabase databases. They looked at about 300,000 websites that use Supabase and found 16,326 databases with tables anyone could read. More than half showed signs of personal data: names, emails, phone numbers, addresses. Some held plain-text passwords, login tokens and one-time codes.
Nothing was hacked. Every one of those apps shipped the public key in its JavaScript, which is normal, and left a table that the public key could read, which is not. UpGuard's researchers simply asked each database for a table called users. If that table didn't exist, the error message often named one that did.
If you built your app with Lovable, Bolt, Cursor, Claude Code or Replit on top of Supabase, here is how to make sure you are not in the next study. You need the Supabase dashboard and ten minutes.
Why this keeps happening
A Supabase app talks to the database straight from the browser. To do that it ships two things in its JavaScript: your project URL and a public key (called the anon key on older projects, the publishable key on newer ones, starting with sb_publishable_). Anyone can open the browser's developer tools and copy both.
That is by design. The public key is supposed to be weak. What makes it weak is Row Level Security (RLS): rules inside Postgres that decide which rows each request may see. With RLS on and sensible policies, the public key can only do what a signed-out visitor should be able to do. With RLS off, the public key can read, and often change, the whole table.
AI coding tools get this wrong in three ways:
- They create tables with SQL or migrations. Tables you make by clicking in the Table Editor get RLS switched on by default. Tables created through SQL, the CLI or an agent do not, unless the SQL says so.
- They "fix" errors by opening everything up. When a query fails because of RLS, the quickest change that makes the error disappear is a policy like
using (true). The app works again, and the table is public again. - They forget the side doors. Views, database functions and storage buckets each have their own rules. A table can be locked while a view on top of it is wide open.
Does Supabase's October 30 change fix this?
Partly. On October 30, 2026, Supabase applies a new default to all existing projects: new tables in the public schema will no longer be reachable through the Data API until you explicitly grant access. That is a good change, because it closes the gap for tables an agent creates after that date.
It does not touch tables you already have. In Supabase's own words, existing tables "keep their current grants and stay reachable". So if a table is open today, it will still be open on October 31. And once an AI tool hits the new permission error, it will usually add the grant and move on, so the same RLS mistakes can come back.
Run the checks below on your existing project.
Check 1: tables with RLS switched off
Open SQL Editor in the Supabase dashboard and run:
select tablename
from pg_tables
where schemaname = 'public' and rowsecurity = false;
Every table this returns can be read by anyone holding your public key, as long as the default grants are in place (on most projects they are). Supabase's Security Advisor flags the same thing as rls_disabled_in_public, with level ERROR.
Fix: switch RLS on for each table. With RLS on and no policies, the table is locked to the public key, which is a safe starting point.
alter table public.your_table enable row level security;
Then add policies for what your app actually needs (check 3).
Check 2: policies that let everyone in
RLS being "on" means nothing if a policy says yes to everyone. Run:
select tablename, policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'public'
and (qual = 'true' or with_check = 'true');
qual is the rule for reading and changing existing rows, with_check the rule for new or updated rows. A value of true means "anyone". Look closely at the roles column: {public} or {anon} includes signed-out visitors.
Some public reads are fine. A table of blog posts or product listings can be readable by everyone. A table with emails, phone numbers, orders, messages or anything a user typed in should never have true for select, and almost no table should have true for insert, update or delete.
Check 3: policies that check the wrong thing
The safe pattern for user-owned data is to compare the row's owner with the signed-in user:
create policy "read own rows" on public.orders
for select to authenticated
using ((select auth.uid()) = user_id);
create policy "update own rows" on public.orders
for update to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
Watch for these mistakes:
to authenticatedwithusing (true). This feels safe because it requires a login, but anyone can sign up. One free account reads every row.- Policies based on
user_metadata. Users can edit their own metadata, so a policy like "role in user_metadata is admin" lets anyone make themselves admin. Supabase's advisor flags this asrls_references_user_metadata. - Update policies without
with check. Without it, a user may be able to move a row to someone else, or change a column they should not touch. - A
roleoris_admincolumn on the profiles table combined with "users can update their own profile". Every user can promote themselves. Keep roles in a separate table that only the server can write.
Check 4: views and functions that skip RLS
Views run with their owner's rights by default, and the owner is usually the postgres role, which ignores RLS. So a view over a locked table can still show every row. On Postgres 15 and later, which covers current Supabase projects, fix it with:
alter view public.your_view set (security_invoker = true);
Database functions marked security definer also run with their owner's rights, and Supabase makes functions in public callable through the API by default. List them:
select p.proname
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public' and p.prosecdef;
For each one, either make it security invoker, or check inside the function that the caller is allowed to do what they ask. If signed-out visitors should never call it:
revoke execute on function public.your_function from anon;
The Security Advisor flags risky views as security_definer_view. Open it once and read everything at level ERROR or WARN.
Check 5: storage buckets
Files live in storage buckets, which have their own rules. Run:
select id, public from storage.buckets;
A bucket with public = true serves every file to anyone who has, or guesses, its URL. That is right for logos and product photos and wrong for invoices, ID scans or user uploads. Also check the policies on storage.objects: a select policy that allows anon lets strangers list every file name in the bucket.
Fix: make private buckets private, give each user a folder named after their user ID, and write policies that check the folder against auth.uid().
Check 6: test it like a stranger
The SQL above tells you what should happen. This tells you what does happen. Copy your project URL and public key from Project Settings → API, then ask for a row count without signing in:
curl -I "https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*" \
-H "apikey: YOUR_PUBLIC_KEY" \
-H "Prefer: count=exact"
Look at the content-range header. The number after the slash is how many rows a stranger can see. For a private table it should be 0, or the request should fail with a permission error. Repeat for every table that holds user data. Then do the same while signed in as a second test account, and check that you see only that account's rows.
This is also how attackers work, and how UpGuard found 16,326 databases. They don't need your password, only the two values your website already gives away.
Bonus: make sure the secret key never reached the browser
Everything above assumes the browser only has the public key. If your service_role key (or a new-style sb_secret_ key) ended up in your JavaScript, RLS does not matter: that key skips every policy. Open your live site, open developer tools, and search all loaded JavaScript for service_role and sb_secret_. If either appears, rotate the key in Supabase today, then move the code that needed it into an Edge Function or server route. Deleting it from the code is not enough, because old copies stay in caches and git history. We wrote more about this in Your API key is probably sitting in your JavaScript.
A prompt you can paste into your AI tool
Audit my Supabase database security. 1) Enable Row Level Security on every table in the public schema. 2) Replace any policy whose USING or WITH CHECK is
trueon tables with user data with policies that compare the row owner to(select auth.uid()), and add WITH CHECK to every update policy. 3) Setsecurity_invoker = trueon every view in public. 4) List every SECURITY DEFINER function and either make it SECURITY INVOKER or add an ownership check, and revoke EXECUTE from anon where signed-out users should not call it. 5) Make storage buckets private unless they only hold public assets. 6) Add explicit GRANT statements for new tables. Do not use the service_role key in client code. Show me every change before applying it.
Then run the six checks again. An AI tool that says it fixed something has not always fixed it.
Or let a scanner do the stranger test
Greenlight Scan runs the outside view for you. The free scan reads your live site the way any visitor's browser does, finds your Supabase project and public key, flags secret keys in your JavaScript and exposed files, and shows you a score, how many problems it found and the title of the worst one.
After you prove you own the site, the deep scan uses your public key to ask each table how many rows a stranger can see. It only asks for a count and never downloads your data. The $19 Full Scan unlocks every finding with the table name, the evidence, why it matters and a fix prompt for Lovable, Cursor, Bolt, Replit, v0 or Claude Code, plus 30 days of re-scans so you can check the fix worked. Here is a sample report so you know what you get.
One limit worth knowing: no outside scan can read your policies, so it can't tell you whether a signed-in user can read another user's rows. Use check 3 and the two-account test for that.
Source: UpGuard Research, "Everything Everywhere: Systemic Data Exposure in Supabase Apps" (2026); Supabase changelog, "Tables not exposed to Data and GraphQL API automatically" (2026).
Check your own app in about 2 minutes.
Scan my app freeThe Friday leak report
One short email a week: the newest ways AI-built apps leak data, and the one-line fix for each. No spam, unsubscribe any time.