How to check if your Supabase tables are readable by anyone
By Pritesh Ghodge, security engineer · Last updated 30 October 2026
If your app was built with Lovable, Bolt, v0 or Cursor on Supabase, your browser talks straight to your database. That's normal. The question is what an anonymous visitor can read when they do the same thing. Here's how to check in about a minute, and how to fix it.
The anon key being public is fine
Your Supabase URL and anon key (newer projects call it the publishable key, starting sb_publishable_) are meant to be in your front-end code. Anyone can open DevTools and copy them. That's by design.
What decides whether that key can read a table is Row Level Security (RLS) and the policies on the table. If RLS is off, or a policy allows everyone, the anon key can read the table.
Two keys must never be in the browser: the service_role key and sb_secret_ keys. They bypass RLS completely. If either is in your bundle, rotate it first, then move that code to a server route or edge function.
The 60-second test
Copy your project URL and anon key from your site's JavaScript (or Supabase → Project Settings → API), then run this for each table:
curl "https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*&limit=1" \
-H "apikey: YOUR_ANON_KEY" \
-H "Authorization: Bearer YOUR_ANON_KEY"[]means no rows came back for an anonymous visitor. Good (or the table is empty).- A row of data means anyone on the internet can read that table. Fix it now.
- An error about permissions means the anon role has no access to the table at all. Also good.
Try the tables that usually hold personal or payment data first: users, profiles, orders, payments, customers, subscriptions, messages, invoices.
Only run this against a project you own.
"RLS enabled" isn't the same as protected
This is the trap that catches people who did turn RLS on. An AI agent hits a "permission denied" error while building, and the quickest fix it finds is a policy like this:
create policy "Enable read access for all users"
on public.profiles for select
using (true);RLS is still on and the dashboard still shows a shield, but using (true) means every row is readable by everyone, including the anon key. Read each policy's using clause and the role it applies to.
The curl test only shows reads. To check INSERT and UPDATE policies too (for example with check (true)), run the free Supabase RLS policy checker: one read-only SQL query, paste the result, and it lists every open policy with fix SQL. It runs in your browser; nothing is uploaded.
The fix
Turn RLS on and allow each signed-in user to read only their own rows. Replace user_id with whichever column holds the owner's id:
alter table public.profiles enable row level security;
drop policy if exists "Enable read access for all users" on public.profiles;
create policy "own rows"
on public.profiles for select
to authenticated
using ((select auth.uid()) = user_id);Add matching policies for insert, update and delete if your app needs them. Then run the curl test again and confirm you get [].
If you're in Lovable or Bolt, you can paste a prompt like: "Enable RLS on every table in the public schema. Remove any policy that uses using (true) for anon or public. Add policies so users can only read and change rows where user_id matches their auth id."
Don't forget storage buckets
Supabase Storage has the same problem with files. A bucket marked public lets anyone with the URL download its files. If a bucket holds user uploads (ID photos, invoices, avatars you'd rather keep private), make it private and serve files with signed URLs. Check the policies on storage.objects too.
What an outside test can't tell you
Testing with the anon key shows what an anonymous visitor can read. It doesn't show whether signed-in user A can read user B's rows. For that, create two test accounts, sign in as one, and try to fetch the other's records by id. You should get nothing back.
It also only covers the tables you test. Unusual table names won't show up in a check of common ones.
Keep checking after every change
Policies drift. A table that was locked down last week can be opened up by a single agent "fix". Re-run the test after every deploy, or let a tool do it.
A reader on X suggested a cheap guardrail that's worth copying: make your CI fail the deploy if any table in the public schema has RLS switched off, or has a read policy that's just using (true). Run this as a step before deploy, with your database connection string stored as a CI secret (never in front-end code):
OFF=$(psql "$DATABASE_URL" -tA -c "select tablename from pg_tables where schemaname = 'public' and not rowsecurity")
OPEN=$(psql "$DATABASE_URL" -tA -c "select tablename || ' / ' || policyname from pg_policies where schemaname = 'public' and cmd in ('SELECT', 'ALL') and qual = 'true'")
if [ -n "$OFF$OPEN" ]; then
echo "RLS off: $OFF"
echo "Open read policies: $OPEN"
exit 1
fiIf a table is meant to be public (a list of blog posts, say), add it to an allow-list in the query rather than removing the check. This catches the "AI turned RLS off to fix an error" regression before it ships. It can't catch a policy that looks strict but compares the wrong column, so keep the two-account test above too.
Abbi Labs runs this check on domains you've verified you own: it finds the Supabase URL and anon key your site already serves, tries to read one row from common tables like the ones above, checks for public storage buckets, and stores only a yes or no. It also looks for leaked secret keys in your JavaScript and exposed .env and .git files. The first scan is free, and Monitor re-scans weekly and after every deploy.
The free quick check needs no sign-up: paste your URL and it tests the same common tables with the public key your site serves, plus secret keys in your JavaScript, /.env, /.git and security headers. Storage buckets and source maps are in the full scan after you verify your domain.
Check my site free, no sign-up →
FAQ
Is it a problem that my Supabase anon key is visible in my site's JavaScript?
No. The anon (or sb_publishable_) key is designed to be public. What protects your data is Row Level Security and table grants. The service_role and sb_secret_ keys are different: they bypass RLS and must never reach the browser.
RLS is enabled on my table. Am I safe?
Not necessarily. A policy like using (true) for the anon or public role keeps RLS switched on while letting anyone read every row. Check what each policy allows, not just whether RLS is on.
Can an outside test prove my app is secure?
No. Testing with the anon key shows what an anonymous visitor can read. It can't show whether one signed-in user can read another user's rows. Test that separately with two real test accounts.
Does Abbi Labs store my data when it checks tables?
No. It requests at most one row per table and stores only a yes or no for whether rows came back.
Related guides
- Free tool: Supabase RLS policy checker
- Lovable app security checklist (from a security engineer)
- Supabase service_role key exposed? Do this in the next 15 minutes
- NEXT_PUBLIC_ and VITE_ variables are shipped to every visitor
- 3 files your production site shouldn't be serving: .env, .git and source maps
- Security headers for Lovable, Bolt, v0 and Cursor apps: CSP, HSTS and clickjacking
- Supabase "42501 permission denied for table": fix it with a GRANT, not by turning off RLS
- Is your Supabase storage bucket public? How to check it and lock it down