Is My Supabase Exposed? Check It in 5 Minutes
Worried your Supabase data is exposed? Run this 5-minute self-audit: check RLS on every table, query your endpoints as a stranger, and spot USING(true) policies.
You opened your browser's developer tools, looked at the network tab, and saw your Supabase URL and a long API key sitting right there in plain sight. Your stomach dropped. Is your data exposed? Is everything your users gave you sitting on the open internet?
Take a breath. Seeing that key is not the problem — it's supposed to be visible. But "is my Supabase exposed?" is still the right question to ask, because some Supabase apps are exposed, and the only way to know which kind you have is to check.
This post is a hands-on self-audit you can run in about five minutes. No code background needed. By the end you'll know whether a stranger can read your users' data, and you'll be able to tell the difference between "exposed" and "safe" with your own eyes.
⚡ TL;DR
- Your Supabase project URL and anon key are public by design. Finding them in your app is not a leak — don't waste time rotating them.
- "Exposed" means a stranger can read or change rows they shouldn't. That happens when a table has RLS (Row Level Security) turned off, or has a policy of
USING (true)that allows everyone.- The honest test is to query your own endpoints as an anonymous user and see what comes back. This post walks you through it step by step.
Step 1: Find your project URL and anon key (and relax about them)
Open your app in a browser, open the developer tools (right-click → Inspect, then the Network tab), and reload the page. You'll see requests going to an address like https://abcdxyz.supabase.co. That's your project URL. In those requests you'll also see a long token labeled apikey or in the Authorization header. That's your anon key (short for "anonymous").
Both of these are meant to be public.
🐺 Not a real problem
The Supabase URL and anon key living in your frontend is by design. Supabase built its entire permission model assuming anyone can see them. Rotating the anon key changes nothing about whether your data is exposed. Move on — the real test is in the next steps.
So why look at them at all? Because you'll need the URL and the anon key in Step 3 to test your endpoints as a stranger would. Copy them somewhere handy. There's one key you should not find here: the service_role key. If you ever see a key labeled service_role in your frontend code or network traffic, stop — that one is a true emergency, and we cover it in service_role vs anon key.
Step 2: Check RLS on every table in the dashboard
Now log in to the Supabase dashboard and open your project. Go to Database → Tables (or Authentication → Policies). You'll see a list of your tables.
For every table that holds user data — profiles, posts, messages, orders, anything personal — you want to see RLS enabled.
RLS, Row Level Security, is the rule your database uses to decide which rows each user is allowed to see. It works like an automatic filter on every query. With RLS off, the database hands back the whole table to anyone who asks. With it on and a real policy in place, it returns only the rows that belong to the person asking. (For the full plain-English version, see Supabase RLS explained.)
Here's what you're looking at:
- RLS enabled on a table with a real policy → good sign. Keep going to Step 3 to confirm.
- RLS disabled on a table with user data → this is almost certainly an exposure. Anyone with your anon key (which is public) can read that table.
Write down which tables have RLS off. Those are your suspects.
Step 3: Query your own endpoints as a stranger
This is the step that turns suspicion into certainty. The dashboard tells you what your settings say. This tells you what your app actually does.
Supabase auto-generates a REST API for your tables at https://YOUR-PROJECT.supabase.co/rest/v1/. You can hit it with nothing but the public anon key — exactly what any stranger could do. If a table returns rows to that request, it's readable by the whole internet.
You don't need to write a program. Paste a command like this into your terminal, swapping in your real project URL, anon key, and a table name:
curl "https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*" \
-H "apikey: YOUR_ANON_KEY" \
-H "Authorization: Bearer YOUR_ANON_KEY"
Now read the response. There are two outcomes that matter:
- Safe: you get back an empty list
[], or an error mentioning permission or RLS. That means the database refused to hand a stranger the data. Good. - Exposed: you get back real rows — names, emails, messages, whatever the table holds. That means anyone on the internet can pull this data with the same one-line command. This is a live leak.
Run that test against each table you flagged in Step 2. The ones that return real rows to an anonymous request are exposed, full stop. Seeing your users' actual data come back in that response is sobering — but it's far better to see it now than to read about it later.
Step 4: Look for the USING(true) trap
There's one more case, and it's the sneaky one. A table can have RLS enabled — the dashboard shows the reassuring green switch — and still return everything to a stranger. That happens when the policy is written as USING (true).
In a policy, the USING part is the condition that decides which rows pass. USING (true) means "this rule is true for every row," so every row passes. RLS is technically on, but it's protecting nothing. AI builders generate this surprisingly often, because USING (true) makes the app work in the preview without anyone having to log in.
This is exactly why Step 3 matters more than Step 2. A table with USING (true) looks protected in the dashboard but fails the stranger test — it returns real rows. If a table shows "RLS enabled" but Step 3 still handed you data, a USING (true) policy is the likely culprit. We dig into this trap, and how to spot it in your policy text, in the USING(true) trap.
So the rule is: trust Step 3 over Step 2. The endpoint test is the truth.
What "exposed" vs "safe" looks like
To make it concrete, here's the side-by-side.
Exposed looks like:
- A table with user data showing RLS disabled in the dashboard.
- An anonymous
curlto that table returning real rows of personal data. - A policy that reads
USING (true).
Safe looks like:
- Every user-data table showing RLS enabled.
- Anonymous requests returning an empty list or a permission error.
- Policies that tie rows to their owner, like
using ( auth.uid() = user_id ).
Notice what's not on either list: the anon key being visible. That never decides whether you're exposed.
How to check your own app
To recap the five-minute audit:
- Find your project URL and anon key in the browser network tab — and relax, they're public by design.
- Open the dashboard and confirm RLS is enabled on every table with user data.
- Query each table as an anonymous stranger with the anon key, and see what comes back.
- Watch for
USING (true)— RLS can be "on" and still leak. - Trust the endpoint test over the dashboard switch. Real rows returned to a stranger means exposed.
Doing this by hand once is a great way to understand your own app. Doing it across every table, every endpoint, and every new feature your AI ships gets tedious fast — which is exactly the part worth automating.
Want this audit run for you, automatically?
Run a free, read-only scan of your live app. Is My Site Hackable? queries your real endpoints the way a stranger would and tells you which tables are exposed — without the anon-key false alarms.
Scan my app free →If you found an exposed table, here's the quick fix
If Step 3 handed you real rows, don't panic — the fix is usually short. Enable RLS and add a policy that ties each row to its owner:
-- Turn on Row Level Security for the table
alter table profiles enable row level security;
-- Let a user read only their own row
create policy "Users read own profile"
on profiles for select
using ( auth.uid() = user_id );
The line that does the real work is using ( auth.uid() = user_id ) — it matches only the logged-in user's rows. Avoid using ( true ). After you apply it, run the Step 3 test again: an anonymous request should now return an empty list. Repeat for every table that leaked. For a complete walkthrough across owner, anon, and other-user, see testing RLS the right way.
FAQ
Does seeing my Supabase anon key mean my app is hacked?
No. The anon key is public by design and is meant to live in your frontend, visible to anyone. Its presence tells you nothing about whether you're exposed. The real question is whether your tables have RLS enabled with policies that restrict rows — which you confirm by querying your endpoints as an anonymous user.
My dashboard shows RLS enabled — am I safe?
Not necessarily. A table can have RLS enabled and still leak everything if its policy is USING (true), which allows every row through. The dashboard switch alone isn't proof. Run the anonymous endpoint test in Step 3; if real rows come back, you're exposed regardless of what the switch says.
What should I do first if I find a leak?
Enable RLS on the affected table and add a real policy that ties rows to their owner (auth.uid() = user_id), then re-test the endpoint to confirm an anonymous request now returns nothing. If you also found a service_role key anywhere public, treat that as a separate, higher-priority emergency and rotate it.
The bottom line
"Is my Supabase exposed?" comes down to one test: can a stranger with your public anon key pull rows they shouldn't? The dashboard hints at the answer, but querying your live endpoints proves it. Your anon key being visible is normal; tables without a real policy are not. An attacker can run that same one-line request in minutes. The point is to run it yourself first — and to keep running it, because your AI ships new tables and new endpoints every time you add a feature.
Find your gaps before an attacker does.
Is My Site Hackable? scans your deployed app for the exact issues in this article — exposed keys, missing RLS, open buckets — and tells you what's real and what's a false alarm.
Run a free scan →