If your Supabase app either shows data it shouldn't or blocks users with
42501 - new row violates row-level security policy, it's almost always one of
six causes. Work down the list; each has a 10-second check.
1. RLS was never enabled (the AI-tool trap)
Supabase enables RLS automatically only for tables created in the Table
Editor. Tables created via SQL editor, migrations, Prisma, or an AI coding
agent ship wide open.
select relname, relrowsecurity
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where nspname = 'public' and relkind = 'r' and not relrowsecurity;
Every row of that result is reachable by anyone holding your public anon key.
2. RLS enabled, but no policy matches ? everything denied
Postgres defaults to deny. If your table suddenly returns empty arrays after
you "turned security on", this is why: relrowsecurity = true with zero
applicable policies denies all rows to all roles.
3. The USING (true) policy that looks like security
create policy "allow_read" on public.profiles for select using (true);
This satisfies "we have RLS" while exposing every row. Search your policies:
select tablename, policyname from pg_policies
where schemame='public' and (qual = '(true)' or with_check = '(true)');
Each hit needs a human decision: is this table meant to be public?
4. Missing WITH CHECK on writes
A SELECT-only mindset leaks into INSERT/UPDATE policies without a WITH clause - and Postgres then derives an implicit check from
CHECKUSING,
which fails in confusing ways (especially on UPDATEs that rewrite the whole
row). Always write both halves explicitly:
create policy "users update own rows"
on public.items for update to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
5. Dependent-policy lockout ("my admin policy is perfect but the table is empty")
Policies that subquery another table (exists (select 1 from profiles where ...))
fail silently when the inner table's own RLS hides its rows from the calling
user. Your admin sees nothing; a bare true policy "fixes" it - which is how
teams get talked into cause #3. Give the inner table a proper read-own-row
policy first.
6. Views, SECURITY DEFINER functions, and Storage
- Post-15 views run as their owner by default ? can expose rows the table
policies hide. Use
security_invoker = trueviews. -
security definerfunctions bypass RLS unless they set it themselves; pinset search_path. - Storage buckets have their own
storage.objectspolicies - a private DB behind a public bucket is still public.
Prove your whole schema in 60 seconds: I put nine read-only queries that
cover causes 1-6 into a free SQL file you run in your own SQL editor - nothing
leaves your database:
github.com/cekuu35/supabase-rls-leak-demo ? audit/rls-audit.sql
The same repo contains a red/green test suite demonstrating the leak and the
one-file fix. If you want the full guided workflow (write-side checks,
role-simulation harness, remediation templates), that's the
Supabase RLS Audit Kit
- $29, runs entirely against your own catalogs.
Disclosure: both links are mine. The audit queries and demo are free and MIT.
Top comments (0)