Yes, under two conditions, and they’re different failures with different fixes. If Row Level Security is off, or switched on with no policy, on a table your Supabase project exposes through its REST API, anyone holding your project’s public anon key can read or write every row in that table. If your service_role key ever reached the browser (a client bundle, a NEXT_PUBLIC_ variable, a mobile app’s decompiled strings), RLS doesn’t matter at all, because that key was built to skip it entirely. The common reassurance that “the anon key is public, so it’s fine” is only true when the first condition doesn’t hold.

Can someone read my whole database with the anon key?

The anon key is supposed to be public. Supabase’s own documentation says so directly: access through a publishable key “is guarded by Postgres via the built-in anon and authenticated roles,” and the safety of exposing that key rests entirely on what those roles are allowed to touch. That’s Row Level Security’s job, not the key’s. The same page’s protection checklist is blunt about what the job actually requires: enable RLS on every table.

Skip that step, or write a policy that doesn’t check ownership, and here’s what the same key can do against a real table, the request itself, project and key redacted:

curl "https://your-project.supabase.co/rest/v1/users?select=*" \
  -H "apikey: $SUPABASE_ANON_KEY" \
  -H "Authorization: Bearer $SUPABASE_ANON_KEY"

The request needs no login, no session, and no password guess. If the users table, or orders, or messages, whichever one holds what you’d call the data, has RLS off or a policy that doesn’t filter by the caller’s identity, that request returns every row Postgres will let the anon role see. With RLS off, that’s all of them. Supabase’s own API security guide states the mechanism directly: “Any granted table without RLS enabled can be accessed by roles with matching Data API grants (for example, anon).” That’s the documented behavior of the API, not something a researcher had to go probing for.

The scale isn’t a guess either. DeepStrike’s research into misconfigured Supabase instances, published September 2025, found “hundreds, possibly thousands” of organizations exposed to this exact class of issue, their words, small side projects and larger companies alike, with no denominator attached. The AxonBuild corpus I audited has one: across 26 real AI-built apps, 9 of 21 third-party apps had an RLS gap, and at least 5 of 21 exposed PII or PHI, some of it reachable without logging in at all.

The anon key is built to be public. Whether a stranger gets rows back is decided by your policies, and by nothing else.

What happens if RLS is off

With RLS off, a table your project exposes through the Data API serves every row it holds to anyone with the anon key. Two separate leak surfaces produce that exposure, and each needs its own check.

Looks safe
Actually safe
The anon key only shows up in your browser bundle, same as every Supabase app
Every table it can reach has RLS enabled with a policy comparing the row to the caller, not just checking whether someone is logged in
You never printed the service_role key anywhere in your own code
It never reached a build step, a client-prefixed env var, or a mobile binary either
RLS is on for the tables you remember creating
RLS is on for every view and every table, including the one a dashboard change added since anyone last checked
Looks safe
The anon key only shows up in your browser bundle, same as every Supabase app
You never printed the service_role key anywhere in your own code
RLS is on for the tables you remember creating
Actually safe
The anon key only shows up in your browser bundle, same as every Supabase app
Every table it can reach has RLS enabled with a policy comparing the row to the caller, not just checking whether someone is logged in
You never printed the service_role key anywhere in your own code
It never reached a build step, a client-prefixed env var, or a mobile binary either
RLS is on for the tables you remember creating
RLS is on for every view and every table, including the one a dashboard change added since anyone last checked
  • RLS-off or ownership-blind tables. The table exists, the anon key can reach it, and either no policy covers it or the one that does only checks that someone is logged in, never who they are. The curl request above is the test.

  • A leaked service_role key. That key bypasses RLS by design, for server code you control. In a client bundle or a public repo, it hands the same bypass to anyone who finds it, no policy gap required.

Both surfaces produce the identical symptom from the outside: a request that should be refused comes back with data. Neither throws an error your monitoring would catch, and neither shows up when you click through your own app, because you’re always logged in as the one account that’s supposed to see what you’re looking at.

One app I audited showed how surface one can reopen without a single line of code changing. A sports-analytics app, exported as a single commit from an AI app builder, had RLS enabled and enforced correctly on every table I tested. None of that lived in the repo, though: every policy, every constraint, and every table definition sat only in the hosted Supabase dashboard, nothing committed anywhere. The security posture I could test in production and the code I could actually read were two different things, and a policy switched off from the dashboard tomorrow would leave no diff, no pull request, nothing for a future reviewer to catch. Whether RLS is on is a fact about what’s running right now, not about what’s committed to a repo, and the two can drift apart with nothing recording it.

How do I check if my Supabase app is leaking?

Two minutes, and none of it needs anyone else’s help.

  1. 01 Open your Supabase dashboard, pick one table that holds real user data, and note its name.
  2. 02 Run the curl request above against that table, using your project URL and the anon key already sitting in your own browser bundle.
  3. 03 If it returns rows you did not create yourself, RLS is off on that table, or its policy does not check ownership. Note it and move to the next table.
  4. 04 Create a second account in your own app if you have not already. Sign in as it and attempt to read, then write, the first account’s row through your app’s normal interface, changing only an ID.
  5. 05 Toggle RLS on for that table in the dashboard and repeat the curl request. If the response goes from full rows to empty or a permission error, that table was open a minute ago.

The fourth step matters as much as the first. A table can pass the anon-key test and still leak, if the policy checks “logged in” and stops there instead of comparing the row to the caller’s own id. That’s the shape a second real account catches and a single curl request doesn’t.

If you find you’re exposed, the fix order

One exception jumps the queue: if a service_role key was the actual leak, rotating it in the Supabase dashboard and moving the new one server-only comes before everything below, since a policy fix does nothing against a key built to skip policies. For the RLS side, the order matters as much as the list:

  1. 01 Enable RLS on the exposed table first, even before the policy is right. A table with RLS on and no policy at all serves nothing to anyone, which beats staying open while you work out the correct rule.
  2. 02 Write the policy next, comparing the row’s owner column to auth.uid(), not one that reads true for convenience.
  3. 03 Rotate anything that was actually visible in the exposed rows: passwords if you rolled your own auth, API tokens if any lived on that table, anything a person could reuse somewhere else once they’ve seen it.
  4. 04 Re-run the same curl request and the two-account test against every table you have, not the one you already found. Whatever generated the first gap usually left more than one.

If you’ve already turned RLS on and started seeing “new row violates row-level security policy” instead, that’s a sign RLS is doing its job, not a new problem. The fix there is a policy that’s too strict, the opposite failure from everything above. Everything above is Postgres-specific to Supabase; the same read-anyone’s-data shape shows up in Firebase’s default rules too, syntax and default state both different, mechanism identical. This read-side gap is also only half of what a database gets wrong: the data-loss bugs that outlast a backup cover the write and delete half, and the same authorization gap behind both is what AI generators default to when nobody tells them to check ownership. Checking this one table is one slice of whether your app is actually ready for real users, not the whole of it.

Common questions about Supabase data exposure

Is the Supabase anon key safe to expose?

Yes, deliberately so. The key only identifies your project; what it can actually reach depends entirely on your Row Level Security policies. It’s safe the way a building’s street address is safe to publish. What matters is whether the doors lock.

Is RLS on by default in Supabase?

It depends on how the table was created. Supabase’s Table Editor in the dashboard enables Row Level Security by default on every table it creates; a table created in raw SQL or through the SQL editor starts with RLS off until someone enables it. That second path is the one AI builders and migration scripts take, and a table with RLS off is fully readable and writable through the anon key as soon as it exists. Several AI builders scaffold the table before anyone writes the policy, leaving exactly that gap open until someone does.

Does disabling RLS break my app, or just make it insecure?

Neither, which is what makes it easy to miss. A table with RLS off returns data on every request the same way a correctly-policied one would, right up until someone who isn’t supposed to see a row asks for it. Nothing in your own testing looks any different.

What’s the difference between the anon key and the service_role key?

The anon key is meant to be public and gets checked against your RLS policies on every request. The service_role key was built to skip RLS entirely, for trusted server code, and should never leave your backend. Handle both correctly and the anon key is the only one that ever appears in a browser.