Is Row Level Security turned off on your Supabase tables? How to check and fix it.
Last reviewed October 16, 2026 against the providers' current documentation.
The short version
Run the Security Advisor in your Supabase dashboard or one SQL query to list tables with RLS off. For each one, turn RLS on, take away the grants your app doesn't need, and add one policy per operation. Then test as a logged-out visitor and as a second user.
Why it matters
Supabase's docs say a table in an exposed schema without RLS can be read and written by any role that has a grant on it. On many projects (Supabase notes not all), new tables in public start with select, insert, update and delete already granted to the anon and authenticated roles. Your anon or publishable key is in your app's JavaScript, so anyone who copies it can use those grants.
Fix it, in this order
Find tables with RLS off
In the Supabase dashboard, open Advisors → Security Advisor. Look for the lints RLS disabled in public and Policy exists RLS disabled (a table that has policies but RLS switched off, so the policies do nothing). You can also run this in the SQL editor:
Every row withselect tablename, rowsecurity from pg_tables where schemaname = 'public' order by rowsecurity, tablename;rowsecurity = falseis unprotected.Turn RLS on
For each unprotected table:
Once RLS is on and there are no policies yet, requests made with the publishable or anon key get no rows back. That's safe, but parts of your app may go blank until you add policies.alter table public.your_table enable row level security;Take back grants the app doesn't need
Policies don't remove grants. Supabase recommends revoking what the client roles hold and granting back only what each one needs:
A request for an operation that isn't granted fails with error 42501 before any policy runs. If a page breaks with that error, check grants before you loosen a policy.revoke all on table public.your_table from anon, authenticated; -- signed-in users manage their own rows; signed-out visitors get nothing grant select, insert, update, delete on table public.your_table to authenticated;Write one policy per operation
Supabase suggests separate policies for select, insert, update and delete rather than onefor allpolicy. For a table with auser_idowner column:
Wrappingcreate policy "Owners read" on public.your_table for select to authenticated using ( (select auth.uid()) = user_id ); create policy "Owners insert" on public.your_table for insert to authenticated with check ( (select auth.uid()) = user_id ); create policy "Owners update" on public.your_table for update to authenticated using ( (select auth.uid()) = user_id ) with check ( (select auth.uid()) = user_id ); create policy "Owners delete" on public.your_table for delete to authenticated using ( (select auth.uid()) = user_id );auth.uid()inselectis Supabase's own recommendation; it lets Postgres evaluate it once per query instead of once per row.Check your views too
Views skip RLS by default because they're usually created by thepostgresuser. On Postgres 15 and later, create views withwith (security_invoker = true)so they follow the policies of the tables underneath. Otherwise, revoke access to the view fromanonandauthenticatedor move it to a schema the API doesn't expose.Test as a stranger and as another user
Open the app in a private window without logging in and load the pages that show data. Then log in as a second test user and confirm you can't see or change the first user's rows. For a repeatable check, Supabase's guide shows how to write database tests and run them withsupabase test db.
Or ask your AI tool to do it
Paste this into Lovable, Bolt, Cursor, Claude Code or whichever tool built your app. Review what it changes before you deploy.
Some of my Supabase tables have Row Level Security turned off. Write a SQL migration that, for every table in the public schema: enables row level security; revokes all privileges from the anon and authenticated roles; grants back only the operations the app actually performs (tell me which and why); and adds one policy per operation (select, insert, update, delete) that limits rows to the owner using (select auth.uid()) = the owner column. Only data that is meant to be public may use using (true), and only for select. Recreate any views in public with security_invoker = true. Never use the service_role or secret key in frontend code to get around RLS. Finally, list each page of the app and the tables it touches so I can test them as a logged-out visitor and as a second user.
How this happens
In Postgres, Row Level Security is off on a new table until something turns it on, so a table created with raw SQL or a migration is unprotected unless that SQL enables it. AI app builders often write that SQL for you, and when a query later fails because of RLS, the quickest change that makes the error go away is to switch RLS off or add a policy that lets everyone in.
Check what your app exposes
The free check reads your live site the way an attacker would: it finds which Supabase project your app talks to and flags any secret key in your JavaScript. To test the RLS policies themselves, add the database tests to the website security report.
Check my app freeQuestions
RLS is on and I have policies. Am I done?
Not quite. Supabase points out that adding policies doesn't take back grants, so also revoke the operations each role doesn't need. Then read each policy and ask who it lets in; using (true) means everyone.
Does the service_role or secret key respect RLS?
No. Supabase's secret keys and the legacy service_role key use a role that bypasses RLS, which is why they must only ever be used on a server.
Can AppInspected test my RLS policies?
The free check can't, because it only reads what a browser can see. The website security report can add database tests that check your Supabase tables using your own test accounts.
Sources
- Supabase: Row Level Security
- Supabase: Advisors (Security Advisor lints)
- Supabase: Securing your data
- Supabase: Understanding API keys
- PostgreSQL: Row Security Policies