Skip to main content
Stackbastion

Draft, not yet published

Supabase row-level security exposed: how to audit your policies in 15 minutes

7 July 2026· 5 min read · by Stackbastion

You turned on row-level security for a couple of tables and figured you were covered. Then you read that RLS is off by default and only protects the tables you explicitly enabled it on. Now you’re not sure which tables are actually locked down and which are wide open to anyone holding your public anon key. Here’s how to find out in about 15 minutes, with real SQL you can paste into the Supabase SQL editor.

Why this happens on Supabase

Supabase gives every project a public anon key. It’s meant to be public, it ships in your frontend, and that’s fine by design. What makes it safe or dangerous is what sits behind it: row-level security policies that decide who can see which rows.

The trap is the default. When you create a table in Postgres, RLS is off. With RLS off, the anon key can read and write every row in that table. Supabase’s dashboard nudges you to enable it, but any table created by a migration, a SQL script, or an AI code generator can slip through with RLS disabled and nobody notices, because the app works either way.

There’s a second, subtler trap. You can have RLS enabled and still be exposed, if the policy you wrote is too permissive. A policy with a using (true) clause and no real condition technically satisfies “RLS is on” while letting everyone read everything. So a proper audit checks two things: which tables have RLS off, and which enabled tables have policies that don’t actually restrict anything.

How to fix it

Open the Supabase SQL editor and run these in order. Each one takes seconds.

Check 1: which tables have RLS turned off entirely.

-- Every row here is a public table with NO row-level security.
-- Reachable by anyone with your anon key.
select
  n.nspname as schema,
  c.relname as table_name,
  c.relrowsecurity as rls_enabled,
  c.relforcerowsecurity as rls_forced
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where c.relkind = 'r'
  and n.nspname = 'public'
  and c.relrowsecurity = false
order by c.relname;

Any row that comes back is a table with the door off its hinges. Write down the names.

Check 2: which tables have RLS on but no policies.

RLS enabled with zero policies means nobody gets in through the anon key, which is safe. RLS enabled with policies is where you have to read carefully. First find tables that have RLS on but no policy at all, so you can confirm they’re intentionally locked:

-- Tables with RLS enabled but zero policies (deny-all, usually safe)
select
  t.tablename
from pg_tables t
where t.schemaname = 'public'
  and t.rowsecurity = true
  and not exists (
    select 1 from pg_policies p
    where p.schemaname = t.schemaname
      and p.tablename = t.tablename
  )
order by t.tablename;

Check 3: the real one. Find policies that let everyone in.

This is the check most people skip. List every policy and its actual condition, so you can spot the ones that don’t restrict anything:

-- Read the qual and with_check columns.
-- A qual of 'true' means the policy allows ALL rows for that command.
select
  tablename,
  policyname,
  cmd,          -- SELECT, INSERT, UPDATE, DELETE, or ALL
  roles,        -- which roles it applies to (look for 'anon' and 'public')
  qual,         -- the USING condition; 'true' = no restriction
  with_check    -- the WITH CHECK condition for writes
from pg_policies
where schemaname = 'public'
order by tablename, cmd;

Read the qual column line by line. Any policy where qual is true (or effectively always true) and roles includes anon or public is an open door dressed up as a locked one. A SELECT policy with qual = true on your orders table means anyone can read every order.

Fix the exposures.

For a table from Check 1 that should be private, enable RLS and add a real policy. A common correct pattern ties rows to the authenticated user:

alter table public.documents enable row level security;

create policy "users read own documents"
on public.documents
for select
to authenticated
using (auth.uid() = user_id);

If you’re not ready to write the right policy under time pressure, enabling RLS with no policy is a safe deny-all until you come back to it. For the too-permissive policies from Check 3, drop them and replace with a conditioned version:

drop policy "old open policy" on public.orders;

create policy "users read own orders"
on public.orders
for select
to authenticated
using (auth.uid() = customer_id);

Re-run Check 1 and Check 3. A clean audit means Check 1 returns nothing that should be private, and Check 3 shows no qual = true policies granted to anon or public on sensitive tables.

Or, we do it for you

RLS misconfiguration is one of the most common holes we find in AI-built apps, and it’s easy to miss one table among thirty. If you’d rather have someone run the full audit and hand you a table-by-table verdict, get a free production audit.

FAQ

Isn’t the anon key supposed to be public? Why is any of this a problem?

The anon key being public is fine. The problem is what it can reach. The key is just a doorknob; RLS policies are the lock. A public doorknob on an unlocked door is the issue, not the doorknob itself. That’s why the audit focuses on policies, not on hiding the key.

I have RLS on for every table. Am I done?

Not necessarily. RLS being on only matters if the policies behind it actually restrict rows. Run Check 3 and read the qual column. A policy with using (true) granted to the anon role passes the “RLS is on” test while letting everyone read everything. That’s the exact case people miss.

Does enabling RLS with no policy break my app?

It will block the anon key from that table until you add a policy, so anything that reads the table through the public key stops working. That’s the point: it fails safe. Add the correct policy and access returns for the right users. Under an active exposure, deny-all first and refine after is the safer order.

What about the service-role key, does RLS protect against that?

No. The service-role key bypasses RLS entirely by design, which is why it must never appear in your frontend. RLS protects the anon key path. If a service-role key has leaked into your bundle, no amount of RLS helps, and you should rotate it immediately and move that call server-side.