Supabase RLS disabled? How a table leaks through the anon key, and how to fix it
Your Supabase URL and anon (publishable) key are meant to ship in your front end. Supabase's docs are explicit about the catch: a table in an exposed schema without RLS is readable and writable by any role with a grant on it, and on many projects new tables in public grant every privilege to anon and authenticated by default. Put together, one table created without RLS can be readable, and writable, by anyone who opens DevTools. This guide is about finding and closing that gap in your own project.
Scan your live app free Passive, no signup, never touches your database.
1. Why a table without RLS leaks
Two checks run before a client touches a table. Grants decide whether a role (anon for signed-out visitors, authenticated for signed-in users) can run an operation at all; RLS policies decide which rows it applies to. Per Supabase's docs, on existing projects a new table in public starts with select, insert, update, delete (and more) granted to anon, authenticated and service_role. If RLS is off, nothing narrows those grants down, so the Data API serves the whole table to anyone holding your public key.
2. Know how tables end up without RLS
Supabase's docs say tables created through the Dashboard have RLS enabled by default, but tables created in the SQL Editor or through another tool (migrations, an ORM, an AI assistant running SQL for you) need RLS enabled explicitly. Views are a second route: by default they run with their creator's rights, so a view over a protected table can hand out every row.
3. Find exposed tables in your own project
Run this in the Supabase SQL Editor. It lists tables in public with RLS off and the privileges anon and authenticated hold on them. Any row returned is reachable through your public key. (Unspill never queries your database; this check runs inside your own project.)
select c.relname as table_name,
c.relrowsecurity as rls_enabled,
has_table_privilege('anon', c.oid, 'select') as anon_can_read,
has_table_privilege('anon', c.oid, 'insert,update,delete') as anon_can_write,
has_table_privilege('authenticated', c.oid, 'select') as authed_can_read
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind in ('r', 'p')
and not c.relrowsecurity
order by c.relname;4. Also check the Security Advisor
Dashboard → Advisors → Security Advisor flags issues such as RLS disabled on tables in public. Treat every finding there as a to-do, and re-run it after schema changes.
5. Fix it: enable RLS, reset grants, add policies
Supabase's documented procedure is: enable RLS, revoke existing grants from the client roles, grant back only what each role needs, then write one policy per operation. Once RLS is enabled, nothing is readable with the publishable key until you add policies, so do this together (ideally in one migration). Example for a table where each row belongs to one signed-in user:
alter table public.notes enable row level security;
revoke all on table public.notes from anon, authenticated;
grant select, insert, update, delete on table public.notes to authenticated;
create policy "Owners read" on public.notes for select
to authenticated using ((select auth.uid()) = user_id);
create policy "Owners insert" on public.notes for insert
to authenticated with check ((select auth.uid()) = user_id);
create policy "Owners update" on public.notes for update
to authenticated using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
create policy "Owners delete" on public.notes for delete
to authenticated using ((select auth.uid()) = user_id);6. Public-read data: grant only select
For content meant to be public (a feed, published posts), Supabase's docs show enabling RLS, revoking everything, granting select only, and a select policy using (true). That keeps it readable without leaving insert/update/delete open:
alter table public.announcements enable row level security;
revoke all on table public.announcements from anon, authenticated;
grant select on table public.announcements to anon, authenticated;
create policy "Anyone can read announcements" on public.announcements
for select to anon, authenticated using (true);7. Fix views too
On Postgres 15+, recreate exposed views with security_invoker = true so they obey the underlying tables' policies. On older versions, revoke anon/authenticated access to the view or move it to a schema that isn't exposed.
8. Stop the next table leaking
Supabase's docs describe two guards: in the Data API settings, turn off 'Automatically expose new tables' (or revoke the default privileges in SQL), so new tables need explicit grants; and use an event trigger to enable RLS on every new table automatically. Then test with pgTAP via supabase test db, asserting both allowed and denied cases for anon and authenticated.
9. If a table was exposed, treat it as an incident
A table that was open to the anon key may have been read or modified. After fixing access, review what the table held, check for unexpected rows or changes, and consider whether you need to tell affected users. Then scan your live app with Unspill to catch related front-end issues, such as a service_role key in your bundle, which bypasses RLS entirely.
Fix guides for these issues
- Verify Supabase Row Level Security: Your Supabase anon key is public by design, so Row Level Security is the only thing protecting your tables. Run these SQL checks to find tables with RLS off or policies that let anyone read everything.
- Supabase service_role key exposed: A Supabase service_role (or sb_secret_) key in front-end code gives anyone full read/write access to your database, bypassing Row Level Security. Here is how to rotate it and fix the leak.
FAQ
Is my data public if RLS is disabled on a Supabase table?
If the table is in an exposed schema (such as public) and the anon role has grants on it, yes. Anyone with your public anon/publishable key, which ships in your front end, can read it through the Data API, and write to it if write grants exist. Supabase's docs say to enable RLS on every table in an exposed schema.
Doesn't the Dashboard enable RLS automatically?
Per Supabase's docs, tables created in the Dashboard have RLS enabled by default, but tables created in the SQL Editor or by other tools need it enabled explicitly. Check with the query in this guide.
Is adding a policy enough?
Not on its own. Supabase's docs note that adding policies doesn't remove grants, and recommend revoking the client roles' grants and granting back only what each role needs. RLS must also be enabled, or policies don't apply.
How is this different from leaking the service_role key?
A table without RLS leaks through the public key everyone already has. A leaked service_role or secret key bypasses RLS on every table. Both are serious; see our anon vs service_role guide for the second.
Sources
- Supabase docs: Row Level Security
- Supabase docs: Securing your API
- Supabase docs: Database Advisors
- Supabase docs: API keys
Last checked against these sources on 2026-10-19. Platforms change; when in doubt, the official docs win.