Skip to content
ShipCheck
Guides

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:

StateWhat anonymous users get
RLS disabledEverything. Every row is readable (and often writable) through the API with the anon key.
RLS enabled, no policiesNothing. 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 definer run with elevated rights. Check them carefully, and revoke execute from anon where 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/delete policies 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).

Check your live app in 20 seconds. ShipCheck's free scan looks for leaked keys, a public Supabase setup, exposed files, missing headers and SEO blind spots. Run a free scan →