Supabase RLS checker: how to test that Row Level Security actually protects your tables
Updated 18 October 2026
Row Level Security (RLS) is the Postgres feature that decides which rows a request may see. In Supabase, it's what stands between your public anon key and your data. A good RLS check answers one question: what can an anonymous visitor actually read or write?
Enabled is not the same as effective
There are three states a table can be in:
| State | What anonymous users get |
|---|---|
| RLS disabled | Everything. Every row is readable (and often writable) through the API with the anon key. |
| RLS enabled, no policies | Nothing. Queries succeed but return zero rows. Safe, but your app may break. |
RLS enabled, permissive policy (e.g. using (true) for anon/public) | Whatever the policy allows, possibly every row. Checkers that only ask "is RLS on?" miss this. |
Tables created in the Supabase Table Editor have RLS enabled by default, but tables created with raw SQL (including many AI-generated migrations) don't unless the SQL enables it.
Step 1: Use Supabase's Security Advisor
In the dashboard, open Advisors → Security Advisor. It flags tables in exposed schemas without RLS, views that bypass RLS, functions with mutable search paths, and more. Fix every error-level item.
Step 2: List RLS status and policies with SQL
-- Which public tables have RLS off?
select relname as table, relrowsecurity as rls_enabled
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'r'
order by rls_enabled, relname;
-- What do the policies allow?
select tablename, policyname, roles, cmd, qual, with_check
from pg_policies where schemaname = 'public';Look for policies whose roles include anon or public with qual = true on tables that hold private data.
Step 3: The real test: read each table anonymously
This is what an attacker does, using only the URL and key from your frontend. Ask for a row count without downloading data:
curl -I "https://<project-ref>.supabase.co/rest/v1/profiles?select=*&limit=1" \
-H "apikey: <anon-or-publishable-key>" \
-H "Authorization: Bearer <anon-key>" \
-H "Prefer: count=exact"
# Look at the Content-Range header: "0-0/1532" means 1,532 rows are readable.(With the newer sb_publishable_… keys, send only the apikey header.) A 401/permission error or a total of 0 means anonymous reads are blocked or filtered. Repeat for every table. Only do this on projects you own.
ShipCheck automates exactly this for verified owners: it finds the Supabase project and public key in your frontend, lists exposed tables, and sends one HEAD request per table asking only for a count. No row data is downloaded or stored. Results show up in your weekly diff, so you'll know if a new table goes public next week.
Step 4: Check the things RLS doesn't cover
- Views: by default, Postgres views run with the permissions of their owner and can bypass RLS. Use
with (security_invoker = true)on Postgres 15+ or move views out of the exposed schema. - RPC functions: functions marked
security definerrun with elevated rights. Check them carefully, and revokeexecutefromanonwhere it isn't needed. - Storage buckets: public buckets serve files to anyone with the URL; private bucket access is controlled by policies on
storage.objects. - Writes: test
insert/update/deletepolicies too (on a test project), not just reads.
Fixing a leaky table
alter table public.profiles enable row level security;
create policy "Users read own profile" on public.profiles
for select to authenticated using ((select auth.uid()) = id);
create policy "Users update own profile" on public.profiles
for update to authenticated using ((select auth.uid()) = id)
with check ((select auth.uid()) = id);After changing policies, re-run the anonymous test to confirm the count drops to zero (or the request is denied).