Skip to content
SniffPup
How it worksSample reportPricingAboutDashboard

Supabase RLS checker: how to check Row Level Security on every table

Short answer: your Supabase anon key ships to every visitor, so Row Level Security (RLS) is the only thing between a stranger and your tables. Check two things: that RLS is on for every table in the public schema, and that no policy lets everyone through (for example using (true) on user data). The SQL and tests below take about ten minutes. Only test projects you own.

Where SniffPup fits: a free SniffPup scan of your live URL checks the outside of your app: secret keys in your HTML and JavaScript (including Supabase service_role and sb_secret_ keys, which bypass RLS), public /.env and /.git files, a public source map and your security headers. It also tells you if your site talks to Supabase. The free scan can't see your RLS policies, because it never touches your database. On the Pro plan ($39/mo), after you prove you own the site, the deep Supabase check uses your site's public anon key, read-only, to count which tables an anonymous visitor can read and which storage buckets are public. It never downloads rows and never writes anything.

1. Run the Supabase Security Advisor

In the Supabase dashboard, open Advisors, then Security Advisor. Look for RLS Disabled in Public (a table anyone with your project URL and key can read and change), Permissive RLS Policy (an always-true policy such as using (true)) and Security Definer View (a view that ignores the RLS of the tables beneath it). Fix every error-level finding before you launch.

2. List tables with RLS turned off

Run this in the SQL editor. Every row it returns is a table the API can serve without any row filtering:

select schemaname, tablename
from pg_tables
where schemaname = 'public'
  and rowsecurity = false;

Turn RLS on for each one, then add policies (with RLS on and no policies, the anon and authenticated roles get no rows at all):

alter table public.your_table enable row level security;

Tables you create in the dashboard Table Editor get RLS by default. Tables created with raw SQL or migrations don't, which is how AI-generated migrations often leave one open.

3. Read your policies

select tablename, policyname, roles, cmd, qual, with_check
from pg_policies
where schemaname = 'public'
order by tablename;

qual is the using condition (which rows can be read or changed) and with_check is what new or updated rows must satisfy. A value of true means everyone in roles passes. That's fine for a public product list, and a leak for profiles, orders or messages.

4. Test with your anon key, the way a stranger would

Copy your project URL and anon (or sb_publishable_) key from your own frontend or the dashboard API settings, then ask for one row of a table that should be private:

curl "https://YOUR_REF.supabase.co/rest/v1/profiles?select=*&limit=1" \
  -H "apikey: YOUR_ANON_KEY" \
  -H "Authorization: Bearer YOUR_ANON_KEY"

If you get a row back, anonymous visitors can read that table. An empty [] means RLS hid the rows (or the table is empty). With a new sb_publishable_ key, send only the apikey header. Reads are only half of it: also check that your insert, update and delete policies don't allow the anon role, using the policy list from step 3 rather than writing test data into production.

5. Test as two different users

Sign up two test accounts. Signed in as user B, try to load user A's rows (in the app, or with the same curl using user B's access token as the Bearer value). If B can see A's data, the policy checks that someone is signed in but not that the row belongs to them. Supabase's database testing docs show how to write pgTAP tests so this stays checked.

Common RLS mistakes

  • using (true) on a table with user data, often a placeholder that was never replaced. Use an owner check such as using ((select auth.uid()) = user_id).
  • An update policy with a real using clause but with check (true), or an insert policy with with check (true): users can write any values, including someone else's user_id.
  • A policy "for authenticated" that only checks the user is signed in. With open sign-ups, that's anyone on the internet.
  • Policies that trust user_metadata from the JWT (for example an is_admin flag). Users can edit their own user_metadata.
  • Views in the public schema created without security_invoker = on, which ignore the RLS on the tables beneath them.
  • A service_role or sb_secret_ key in the frontend. It bypasses RLS completely, so perfect policies don't help. Rotate it and move that code server-side.
  • Public storage buckets holding user uploads. Public is fine for logos, not for invoices or IDs.

What SniffPup checks

The free scan of your published URL looks for leaked secrets (masked in the report), public .env/.git/.npmrc/.DS_Store files, the source map your JavaScript points to, security headers, cookie flags, HTTPS redirects and what AI crawlers can read. It lists every issue by title and severity and shows full details and the fix for the most serious one. A one-time $19 Full Fix Report unlocks the explanation, evidence and copy-paste fix prompt for every finding plus 30 days of re-scans; plans start at $9/mo. The deep Supabase check described above is part of Pro. See exactly what the scanner does, pricing and the short post Is your Supabase table open to everyone?

FAQ

Can SniffPup's free scan check my Supabase RLS policies?

No. The free scan only reads what your live website serves to the browser. It can't see your database or your policies. When it finds your Supabase project URL in your HTML or JavaScript, it tells you your site talks to Supabase and notes whether the public anon or publishable key is there too (that one is meant to be public). It also flags a service_role or sb_secret key as critical, because those bypass Row Level Security.

What does the Pro deep Supabase check do?

After you prove you own the site (a DNS TXT record or a /.well-known file), the Pro deep check uses your site's public anon key, read-only. It lists the tables your API exposes (up to 30, or tries common names like users, profiles and orders if that list isn't available), asks for a row count on each without downloading any rows, and reports tables an anonymous visitor can read. It also lists storage buckets it can see that are marked public. It never inserts, updates or deletes anything and never stores row data.

Does a clean deep check mean my RLS is correct?

No. It only tests anonymous reads. It doesn't test writes, signed-in users reading each other's rows, or tables outside the ones it can find. Run the SQL checks and the two-user test in this guide as well.

Is it safe that my Supabase anon key is in my frontend?

Yes, the anon or sb_publishable_ key is designed to be public. It is only safe if every table in an exposed schema has RLS turned on with policies that match who should see what.

Can SniffPup check my mobile app's Supabase backend?

Only through a live website. SniffPup scans web URLs; it can't scan native mobile apps.

A clean report is not a guarantee of security.

Run a free scan →

SniffPup

SniffPup is a product of Ember and Saru LLC.
10850 Providence Rd #1370, Charlotte, NC 28277, USA

Support: support@sniffpup.com
Abuse & scan opt-out: abuse@sniffpup.com

ProductFree scanSample reportPricingHow our scanner worksBlog
CompanyAboutSniffPupScanner bot & opt-outsecurity.txtContact
LegalTermsPrivacyAcceptable UseRefunds

SniffPup checks what is publicly visible on your site. A clean report is not a guarantee of security. © 2026 Ember and Saru LLC