16,326 Open Supabase Databases: The RLS Checklist for Agent-Built Apps
UpGuard found 16,326 Supabase databases readable by anyone. How agent-built backends end up open, a five-minute curl test, and the RLS fix.
UpGuard found 16,326 Supabase databases readable by anyone. How agent-built backends end up open, a five-minute curl test, and the RLS fix.
On September 25, UpGuard Research published the largest study of Supabase misconfigurations to date: 16,326 Supabase databases readable by anyone on the internet, no login, no exploit, just a request. More than half showed signs of personal information. The exposed apps ranged from an adult streaming site’s private conversations to a foreign consulate’s records. Supabase itself was not hacked. The apps built on it were configured, or rather not configured, by people and agents who never wrote a security policy.
If an AI coding tool built your backend in the last year, there is a real chance one of those databases is yours. This post explains exactly how a Supabase table ends up public, why agent-built apps are more exposed than hand-built ones, a five-minute test you can run right now with nothing but curl, the fix, and how to make your agent do the right thing by default so you never have to think about it again.
Supabase gives every project two keys. The anon key is designed to be public. It ships in your web app, your mobile app, your browser bundle, and anyone who opens DevTools can read it. That is fine by design, because the anon key on its own is only supposed to see what your row-level security (RLS) policies allow it to see.
Here is the part that catches people. Supabase exposes every table in your public schema through an auto-generated REST API. If a table has RLS disabled, that API returns every row in it to anyone holding the anon key. And RLS is not on by default everywhere. Create a table in the dashboard’s table editor and the "enable row level security" box is ticked for you. Create the same table with a SQL statement, a migration file or an API call, and RLS is off unless you explicitly turn it on.
That second path is exactly how Cursor, Claude Code, Bolt, Lovable and every other coding agent builds a backend. The agent writes a migration with create table, wires the anon key into the client, runs the app, and everything works. It works because the anon key can read everything. Nobody wrote the policy, the tests passed, and the app shipped. UpGuard notes that this pattern has been reported since March 2025, when a developer first found it across apps generated by one vibe-coding platform, and that Supabase’s newer safeguards are not automatically applied to tables created through coding tools.
A developer who has been burned once enables RLS out of habit. An agent has no habits, only instructions, and the instruction it was given was "make the app work". Three things compound.
The result is a backend that is correct in every way a test can check and open in the one way a test never checks: what a stranger with your anon key can read.
You do not need a security tool. You need your project URL, your anon key (it is in your client code) and curl. For each table, run:
curl "https://YOUR_PROJECT.supabase.co/rest/v1/YOUR_TABLE?select=*&limit=5" -H "apikey: YOUR_ANON_KEY" -H "Authorization: Bearer YOUR_ANON_KEY"
If rows come back for a table that holds anything private, users, orders, messages, tokens, that table is open to the internet. If you get an empty array, RLS is on and no policy allows anonymous reads, which is what you want for private data. If you get a permission error, the table is not exposed through the API at all.
To get the list of tables to test, open the SQL editor and run select tablename, rowsecurity from pg_tables where schemaname = 'public';. Any row with rowsecurity = false is a table with RLS off. Supabase’s dashboard also has a Security Advisor under Database that flags exactly these tables, so use it, but run the curl test too, because the advisor tells you RLS is on and the curl test tells you whether your policies actually hold.
Do this for every table, including the boring ones. The UpGuard study found one-time password codes and payment data sitting in tables nobody thought of as sensitive.
Enabling RLS takes one statement per table:
alter table public.YOUR_TABLE enable row level security;
The moment you run that, the anon key gets nothing from the table. That is the safe default, and it is also why agents avoid it: with RLS on and no policies, your app’s reads return empty. So the second step is writing the policies that describe who may see and change which rows. The common shape for user-owned data is a user_id column and a policy per operation:
create policy "users read own rows" on public.YOUR_TABLE for select using (auth.uid() = user_id);
create policy "users insert own rows" on public.YOUR_TABLE for insert with check (auth.uid() = user_id);
Add update and delete policies in the same shape. For genuinely public data, a read-all policy is fine: for select using (true). What you should never do is "fix" a broken read by disabling RLS again or by moving the service role key into the client.
The anon key is public and safe only because RLS limits what it can see. The service role key bypasses RLS completely. It belongs on a server you control, in an environment variable, and nowhere else: not in a client bundle, not in a mobile app, not in a public repo, not in a prompt you paste into a chat. If it has ever been in any of those places, rotate it in the dashboard today. UpGuard’s exposed set included databases where the service key was doing the anon key’s job.
Supabase Storage uses RLS policies too. A bucket marked public serves every object to anyone with the URL. If your agent created a bucket for user uploads and made it public because the image would not load otherwise, the fix is a private bucket plus a policy, or signed URLs, not a public bucket.
Fixing today’s tables is the easy part. The point is that the next table your agent creates should be safe without you remembering. That is an instructions problem, and it is solvable in an afternoon.
Supabase is not the problem. It is one of the best backends a solo builder can pick, and it has spent two years adding safeguards, advisors and defaults to stop exactly this. The problem is that the fastest way to build an app in 2026 is to have an agent write the migrations, and agents do not write security policies unless you tell them to. Sixteen thousand databases are open because sixteen thousand builders got a green test and shipped.
The same week UpGuard published its study, Lovable announced $600 million in annualised revenue. Vibe coding is not slowing down, so the fix has to live in the instructions, not in your memory. Run the curl test today, then make your agent run it forever.
Shipped something and want other builders to see it? List it on HelloBuilder. One URL, and the page writes itself.
Get the weekly digest for AI builders & vibe coders. Curated tools, resources, and stories. Skip the scroll.