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.
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.
- 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/. - 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.
- 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. - 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.
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 );
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.
Read next
- Your Supabase table has no row-level security, so your anon key reads all of itWhat an attacker sees before they touch your site
- Your assetlinks.json and apple-app-site-association are public by design. Here's what they tell people.What an attacker sees before they touch your site
- What can someone learn about your site without hacking it?What an attacker sees before they touch your site
- Your tech stack is public. Does that matter?What an attacker sees before they touch your site
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.