hellobuilder

Command Palette

Search for a command to run...

← Back to blog

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.

Nishant Modi
September 28, 2026 · 9 min read

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.

How a Supabase table ends up readable by anyone

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.

Why agent-built backends are worse than hand-built ones

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 agent optimises for green. If reads succeed with the anon key, the feature is done. RLS makes reads fail until policies exist, so from the agent’s point of view enabling it breaks the app.
  • The agent copies the keys it finds. Service role keys, which bypass RLS entirely, end up in client code and public repos because the agent saw them in .env and needed a request to work. UpGuard specifically calls out public keys being treated as secret keys and vice versa.
  • Nobody reviews the migration. The SQL an agent writes is the least-read code in a vibe-coded project. It runs once, it works, and it is never opened again.

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.

The five-minute test

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.

The fix: turn RLS on, then write policies

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.

Anon key versus service role key

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.

Storage buckets follow the same rule

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.

Make the agent do it by default

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.

  1. Put the rule in your instruction file. In CLAUDE.md or AGENTS.md, one paragraph: every migration that creates a table in the public schema must enable row level security in the same migration and add select, insert, update and delete policies scoped to auth.uid() unless the table is documented as public. The service role key is never referenced in client code.
  2. Give the agent the test. Add the curl check above as a script in the repo, and tell the agent to run it against every new table before it reports a feature as done. Agents are good at running the checks you hand them and terrible at inventing the checks you did not.
  3. Gate it in CI. A query against pg_tables for rowsecurity = false in the public schema, failing the build on any result, catches the case where the agent ignored the instruction.
  4. Tools that enforce project rules on every edit exist now. Abide, released this month, asks a decision model one question per rule on every diff and makes the agent fix violations in the same turn. "Never create a public-schema table without RLS" is exactly the kind of rule no linter can check and a decision model can.
  5. Run Supabase’s Security Advisor after every migration and treat its RLS warnings as build failures, not suggestions.

The checklist

  • List every table in the public schema and its rowsecurity flag.
  • Run the curl test with the anon key against every table. Empty array or error is good. Rows are bad.
  • Enable RLS on every table that returned rows and should not have.
  • Write select, insert, update and delete policies scoped to auth.uid() for user-owned data.
  • Search your client code and git history for the service role key. If found, rotate it.
  • Check storage buckets: private by default, signed URLs for private files.
  • Add the RLS rule to CLAUDE.md or AGENTS.md and the curl test to the repo.
  • Add a CI check that fails on any public-schema table with RLS disabled.

The honest read

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.

AI is moving fast. Don't get left behind.

Get the weekly digest for AI builders & vibe coders. Curated tools, resources, and stories. Skip the scroll.

Keep reading