What is RLS (Row Level Security)? The setting that keeps your Supabase data private
In one sentence:Row Level Security is the Postgres rule set that decides, row by row, who can read or write data, and in Supabase it's the only thing standing between your public anon key and your entire database.
What it actually is
Row Level Security (RLS) is a Postgres feature (every Supabase project is a Postgres database underneath) that decides which individual rows of a table a request can see or touch, not just whether it can reach the table at all. Think of your API key as the key card that gets someone into the building lobby; RLS is the lock on each apartment door. Being in the building doesn’t mean every door opens for you. Once RLS is on for a table, every query (from your app, the auto-generated REST API, even the SQL Editor) gets an invisible WHERE clause tacked on, built from policies you write, like “only rows where user_id matches whoever is logged in.”
Why your AI just did this
Supabase turns tables in your public schema into a REST endpoint via PostgREST. That part hasn’t changed. What changed: since May 30, 2026, new projects stop granting anon and authenticated access to a table the moment it’s created, so a fresh table now returns a permission-denied error until someone runs grant select on public.<table> to anon;. Projects started earlier still auto-expose new tables the old way. Either way, once a table is reachable, RLS decides which rows come back. That’s why your AI enables it and writes policies. Without them, a reachable table answers to anyone holding your anon key, which sits inside your frontend bundle, visible in dev tools. So when your AI runs alter table profiles enable row level security; and create policy "select own profile" on profiles for select using ((select auth.uid()) = user_id);, it makes sure select * from profiles from a logged-in user only returns that user’s own row.
When you’ll run into it
- You ask your AI “why is my query returning nothing?” after adding a new table. The answer is often RLS on with zero policies attached, and the default with no policies is deny-everything, not allow-everything.
- On a project started after May 30, 2026, a fresh table returns a permission-denied error on first query. A missing
grant, separate from RLS. - Someone flags “your Supabase is public?” after opening your site’s network tab, grabbing the visible
anonkey, and testing whether your tables respond without logging in. - You create a table through the Table Editor (RLS turns on automatically) versus the SQL Editor, a migration, or an ORM like Prisma (RLS stays off until you run
enable row level securityyourself). Same project, two different defaults.
What to check
- Run
select tablename, rowsecurity from pg_tables where schemaname = 'public';(or Database → Policies in the dashboard) to see which tables have RLS on - For tables with RLS on, confirm at least one policy exists. RLS with no policies blocks all access, which looks like a bug but is the safe default
- For tables with RLS off, confirm nothing sensitive is in them and that
anon/authenticatedaren’t granted on them. Pre-May 2026 projects grant new tables automatically, so “RLS off” can still mean wide open - Check policies use
(select auth.uid()), not a bareusing (true). That phrase means everyone, no restriction, and theselectwrapper is Supabase’s current recommended form - If you added an event trigger to auto-enable RLS on new tables, it only covers tables created afterward. Existing ones still need RLS by hand
Say it like a dev
Instead of: “Make sure random people can’t read other users’ data.”
Say: “Enable RLS on the orders table and add a policy so a user can only select and update rows where user_id matches (select auth.uid()).”
Instead of: “Why is my table empty when I query it from the app?” Say: “Check whether RLS is enabled on this table with no matching policy for the current role. That would explain the empty result instead of an error.”
Instead of: “Lock down the database.”
Say: “Audit every table in the public schema for RLS status and grants, and list any policy that uses using (true).”
People actually ask
“I am new to Supabase and I am confused how querying data works”
Every query you send, from your app or from the SQL Editor, first passes through Postgres's grants, then through Row Level Security policies before you see a single row. If a table has RLS on and no policy covers your case, the query succeeds but returns nothing, which is why data can seem to vanish even though it's in the database.
“your Supabase is public?”
Your Supabase project's URL and `anon` key are meant to be visible in your frontend code, so that part is normal. What makes a project actually public is a table the `anon` role can reach (automatic on projects created before May 30, 2026, or after an explicit `grant` on newer ones) that also has RLS off, or RLS on with a policy like `using (true)`. That combination is what lets anyone with the anon key read or write rows they shouldn't.
“Do I need RLS if my app already checks permissions in the frontend?”
Yes. Frontend checks are cosmetic, not security. Once a table is reachable through Supabase's REST API, anyone can bypass your app entirely and call that API directly with the anon key; RLS is what decides which rows they get back at that layer, not your frontend code.
Related terms
Go deeper
Checked against
- https://supabase.com/docs/guides/database/postgres/row-level-security
- https://supabase.com/docs/guides/api/securing-your-api
- https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically
- https://supabase.com/docs/guides/troubleshooting/rls-performance-and-best-practices-Z5Jjwv
Glim can explain your sessions, live.
The app watches your coding agent and narrates it in plain words. Waitlist is open.