By the SurfaceCheckr team. Every finding maps to a check we run in production - see our methodology.

Supabase security: what a stranger can reach with your public key

Supabase gives your browser a direct line to a Postgres database. That is the feature, and it is a good one: no backend to write, the client just queries the data. It is also the thing to understand before you ship, because it changes where your security actually lives. It is not in the key. It is in the rules on your tables.

That one shift trips up almost everyone who moves fast with Supabase, and it is the root of nearly every Supabase exposure a scan finds from outside. So this is the short version of what a stranger holding your public key can and cannot reach, and how to check which side of that line your project is on.

The anon key is public on purpose

The first instinct when you find the Supabase key in your bundle is to panic and rotate it. Don't. The anon key is designed to be public. Supabase's own docs list it as safe to expose "online: web page, mobile or desktop app, GitHub actions, CLIs, source code." Finding it in your JavaScript is not the bug.

What Supabase is equally clear about is what the key does not do. In their words, publishable keys are not meant to protect against code analysis or network inspection, because "it is inevitable" that they get retrieved from your public components. The key is a way to reach the API. The thing that decides what the API hands back is row-level security. Supabase says it plainly: "access to your project's data is guarded by Postgres via the built-in anon and authenticated roles," and full protection means enabling RLS on all tables.

So the question is never "is my key exposed." It is "what can that public key reach when it asks."

Where the gate goes missing

Row-level security is a set of policies you attach to each table: rules like "a user can select only rows where user_id equals their own id." With RLS on and a policy written, the table answers correctly. With RLS never turned on, the table answers everyone.

The trap is that whether a new table gets RLS depends on how you made it. Supabase enables RLS by default on tables created through the dashboard's Table Editor. Tables created in raw SQL or a migration do not get it automatically, and Supabase says so: "If you create one in raw SQL or with the SQL editor, remember to enable RLS yourself." That is exactly the path an AI assistant or a migration-first workflow takes. The table ships, the app works, and nobody wrote the rule.

same queryselect * from profiles;
anon key · RLS policy in place
only the rows the policy allows
anon key · RLS never enabled
every row, every column, to anyone
Same public key, same query. The only variable is whether the table has a rule. No rule means the whole table is readable from the browser.

There is a second, worse key to know about. The service_role key bypasses row-level security entirely, by design, so the server can do admin work. It is a secret and must never reach the browser. If it ships in your bundle, RLS stops mattering at all: whoever holds it reads and writes every table as an admin. That is a different and more severe problem than an open table, and it has its own writeup.

How to check your project from outside

You do not need to log in to find the common problems. A stranger can't either, which is the point.

  1. Open your live app in an incognito window with no session. Open devtools, watch the network tab, and reload. Look for requests to *.supabase.co/rest/v1/.
  2. For each table those requests hit, ask whether a logged-out visitor should read it. Replay the same request in the console with no auth. If the data still comes back, RLS is not protecting that table.
  3. Search your JavaScript bundle for service_role. It should never be there. If it is, treat it as an active incident: rotate it and move it server-side.
  4. In the Supabase dashboard, the database advisor flags every table with "RLS disabled in public." That report is the fast, authoritative version of steps 1 and 2.
$ your-project.supabase.co
what the street sees
anon key in bundlepublic by design, not a finding
GET /rest/v1/profilesreturns rows with no session
RLS on profilesnever enabled
service_role in bundlenot found, good
What an outsider learns about a Supabase project by watching the network tab. The key is fine. The open table is the finding.

The fix is a policy, never a hidden key

You cannot fix an open table by hiding the anon key, because you can't hide it and rotating it changes nothing. The fix is to enable RLS on every table in an exposed schema, then grant exactly the access each feature needs.

-- table from a migration:
-- RLS never enabled, so the public
-- anon key reads every row
create table profiles (
id uuid, email text, role text
);
Enable RLS, then add the narrow policy. Until a policy exists, an RLS-enabled table denies all API reads, which is the safe default.

The pattern generalizes past this one table. A public signup form needs insert, not select. A user's dashboard needs select where the row belongs to them. The moment you write the narrow rule, the public key goes back to being what it was meant to be: a way to reach the API, gated by a policy you control.

This is all checkable from outside, because whether a table answers a stranger's query is exactly what a stranger can test. A passive external scan reads what your project already serves to an unauthenticated request and flags the shape of the leak, a frontend call that returns other people's data. It sees the leak; it can't see your policy file, so treat a finding as "go read this table's rules." SurfaceCheckr does that read the same way an outsider would, no login and no attack, and it is honest about the limit: it won't test your authenticated logic or tell you whether a policy is subtly wrong, only whether the table is answering the public at all. The best next step is usually turning RLS on for the table it flagged. If you are building this way, the vibe coding security guide covers the neighbouring exposures too.

Find it before someone else does.

Paste your domain. The grade and issue count are free, and you'll see in a couple of minutes exactly what's reachable from outside.

External, read-only scan. We only request public URLs - never log in, never send attack traffic.