Supabase RLS Not Working? The 7 Causes We See Most
Quick answer
If RLS seems 'not working' in Supabase, the table is usually exposed because the policy exists but grants too much, the policy was never created on a table that had RLS force-enabled later, or a service_role key leaked into client code and bypasses RLS entirely. Audit every table with `select * from pg_policies` and test with the anon key, not the dashboard.
1. RLS is enabled but no policies exist
This is the most common cause, and the most surprising: enabling Row Level Security with zero policies blocks access for regular roles — but table owners and the service_role bypass it. If you created the table via the SQL editor while logged in as postgres, your own queries work fine while every anon request is denied (or, worse, an old permissive default is still in place).
Fix: list every policy per table and confirm one exists for each operation you actually need:
- select * from pg_policies where schemaname = 'public';
- For each public table, verify there is a policy for SELECT/INSERT/UPDATE/DELETE as intended.
- If a table should be private, it needs zero public policies — that's valid.
2. A policy grants more than you think
A single `create policy "public_read" on t for select using (true)` makes the whole table readable by anyone with the anon key. It's the fastest way to ship, and the fastest way to leak every row. The same applies to policies written as `using (true)` for writes.
Fix: scope every policy to the requester — `using (auth.uid() = user_id)` — and add `with check` on insert/update so users can't write rows they can't read.
3. The service_role key is in the client
The service_role key bypasses RLS completely. When it ends up in frontend code — an env var prefixed with VITE_, a hardcoded string, or a Lovable/Bolt/v0-generated client — RLS becomes decoration: every request runs as the service role.
Fix: search the repo for the key pattern, rotate the key in the Supabase dashboard, and keep it server-side only. StackShield's GitHub scan catches this exact pattern, including in code that was never committed to main.
4. force row level security was never applied
By default, table owners bypass RLS. If rows were written through an owner-role path, an audit can look clean while the table is still wide open to the anon role. This bites especially on tables migrated between projects.
Fix: `alter table t force row level security;` for sensitive tables, then re-test as anon.
5. Public grants on the table itself
RLS policies and GRANTs are separate layers. If `grant select on t to anon` was run (some AI-generated SQL does this), the policy layer's intent is undermined at the privilege layer.
Fix: `revoke all on t from anon;` unless the table is genuinely public, and let policies govern access.
6. RLS enabled after the table went live
If RLS was enabled after launch, any cached clients, edge functions, or replicas that read via the old access path may still see data. Also, enabling RLS without immediately adding policies locks out legitimate reads and tempts a fast 'fix': `using (true)`.
Fix: enable RLS and add scoped policies in the same migration; never as a reactive one-liner.
7. Testing with the wrong role
The Supabase dashboard runs as an elevated role, so 'it works in the dashboard' proves nothing about what the anon key can do. Most teams discover exposure from a user, a scan, or a breach — not from testing.
Fix: test with a real anon request (curl with the publishable key) for every public table, every time you change a policy.
RLS misconfiguration is the #1 finding in vibe-coded apps we scan — usually cause #1 or #3 above. StackShield connects to your Supabase project, audits every table's policies and grants as the anon role, and proposes a scoped fix for each issue for your approval. Run your first scan free.
Run a free scan