What is RLS in Supabase? Row Level Security explained
RLS (Row Level Security) is a database setting that decides, row by row, what each visitor may read or change in a Supabase table. Because a Supabase app talks to its database straight from the browser with a public key, every table needs RLS turned on plus a policy such as (select auth.uid()) = user_id, or anyone can read it.
If you built an app with Lovable, Bolt or Cursor, you've probably seen "RLS" in a warning or a red badge. Here's what it means and how to check your project. No database background needed.
What is RLS in Supabase?
RLS is a rule set attached to a database table that filters every request, so each person only gets the rows they are allowed to see. A row is one record: one user profile, one order, one note. A policy is a single rule, written in SQL (the database's language), such as "a signed-in user can read rows where user_id is their own ID." RLS is a feature of Postgres, the database Supabase runs on.
Why does Supabase need RLS so badly?
In a traditional app, only your server talks to the database, and your server code decides who sees what. Supabase skips that middle step: your app calls the database directly from the user's browser, using a public key (called the anon key or publishable key). That key ships inside your website's code, where anyone can copy it. That's by design, and the anon key is safe only because RLS sits behind it.
So the database itself has to decide who gets which rows. Think of a hotel where every guest receives the same key card. The card gets you into the building. RLS is the lock on each room door that checks whether this room is yours. With RLS off, the same card opens every room.
This has gone wrong at scale. In May 2025, researcher Matt Palmer disclosed CVE-2025-48757: Lovable-built apps with missing or insufficient RLS let anyone holding the public key read, and in some cases write, their Supabase tables. His scan found 303 exposed endpoints across 170 apps, with names, phone numbers, payment details and API keys among the data. Full story: the Lovable RLS vulnerability.
What does "RLS disabled" mean in Supabase?
"RLS disabled" means Postgres skips the row check entirely for that table. Whoever can reach the table through Supabase's API gets every row.
In the Table Editor, Supabase marks this with a red Unrestricted badge next to the table name. It means RLS is off and the public roles can read the table. Our post on the Supabase Unrestricted badge decodes every variant of that warning.
AI builders often cause this by accident. A query returns nothing, the AI "fixes" it, and the fix is one line:
-- 0001_init.sql
create table public.notes (
id bigint generated always as identity primary key,
user_id uuid not null references auth.users (id),
body text not null
);
-- Added because the app showed "no rows" and this made it work.
alter table public.notes disable row level security;
- Criticalsupabase/migrations/0001_init.sql:9
Row Level Security disabled
Re-enable RLS (ALTER TABLE <t> ENABLE ROW LEVEL SECURITY;) and add policies that scope access with auth.uid(). - Warningsupabase/migrations/0001_init.sql:2
Table created without enabling RLS
Add ALTER TABLE <table> ENABLE ROW LEVEL SECURITY; plus access policies right after the CREATE TABLE, and grant anon / authenticated only the privileges the app needs, per table, after RLS is on. A later migration in the same scan that enables RLS (or drops the table) clears this warning. It still shows when the enable is in a file whose run order is unknown (a schema.sql with no version number) or not part of this scan (a pull-request scan sees only the files the PR changes).
The app works again because nothing is filtered anymore, for anyone. One nuance: since October 30, 2026, tables you create need an explicit grant before the API can reach them at all. Tables that existed before that date kept their access, so an old table with RLS off is readable right now.
What is an RLS policy?
A policy is a one-line condition Postgres checks for every row. If the condition is true for this visitor and this row, the row comes back. If no policy allows it, the row stays hidden.
Two words you'll see in every policy: using (...) holds the condition for reading and finding rows, and with check (...) holds the condition a new or edited row must pass. auth.uid() is a built-in Supabase function that returns the signed-in user's ID, or nothing if no one is signed in.
Example 1: users see only their own rows.
-- Turn RLS on. With no policies yet, the table returns nothing.
alter table public.notes enable row level security;
-- Signed-in users can read rows whose user_id is their own ID.
create policy "Users read own notes"
on public.notes for select
to authenticated
using ( (select auth.uid()) = user_id );
The (select ...) wrapper just makes Postgres compute the ID once per query instead of once per row.
Example 2: a public, read-only table.
alter table public.blog_posts enable row level security;
-- Anyone, signed in or not, can read posts.
create policy "Anyone can read posts"
on public.blog_posts for select
to anon, authenticated
using ( true );
-- No insert, update or delete policies, so nobody can write through the API.
using (true) means "every row passes." That's fine here because blog posts are meant to be public.
Example 3: the classic mistake.
-- Looks like security. Works like a public table.
create policy "Allow all"
on public.orders for select
using ( true );
Same using (true), on private data. The Unrestricted badge disappears, and every customer's order is still readable by anyone. We wrote a whole post on the using (true) trap because it's so common.
Why "RLS on" isn't the same as "RLS right"
Turning RLS on is step one. The policies decide whether you're protected, and two mistakes catch most beginners.
"Signed in" isn't a security check if anyone can sign up. A policy written to authenticated using (true) reads as "only logged-in users." But if your app has a public sign-up page, anyone on the internet can make an account in ten seconds and read every row. Scope policies to the user ((select auth.uid()) = user_id), not just to "someone is logged in."
RLS picks rows, not columns. Say users can update their own profile with using ((select auth.uid()) = id). That stops them editing other people's profiles. It doesn't stop them editing any column of their own row, including role, is_admin or plan. One request from the browser console and a free user is on your paid plan. Keep fields like those in a separate table users can't write to, or lock the columns down (covered in the complete guide linked below).
What is "enable automatic RLS" in Supabase?
It's an option that turns RLS on by itself for every new table, so a table can't be born unprotected.
Tables you create in Supabase's Table Editor get RLS switched on automatically. Tables created with SQL, which is how AI builders and migrations do it, start with RLS off.
Supabase's fix is a Postgres event trigger: a small function that runs after every create table and enables RLS on the new table. The event triggers docs give the SQL for one named ensure_rls, and Supabase's 2025 security retro says it added a one-click setup for it in the dashboard. Know its limits:
- It only affects tables created after you install it. Existing tables still need RLS turned on by hand.
- It only covers tables in the
publicschema. - It turns RLS on but writes no policies. The new table returns nothing until you add policies, and that's exactly when someone is tempted to add
using (true).
How do I check my Supabase project for RLS problems?
Open the SQL Editor in your Supabase dashboard and run:
select tablename
from pg_tables
where schemaname = 'public'
and not rowsecurity;
Every table it lists has RLS off. Turn it on with alter table public.<name> enable row level security; and add policies, or your app will stop seeing that data.
Then test like an outsider. The SQL Editor runs as an admin that skips RLS, so it can't test policies. Instead, sign up with a second account and try to view, edit or delete the first account's data, for example by changing an ID in the URL. If you can see someone else's rows, a policy is wrong.
If you'd rather not do this table by table, GuardLayer's free Live check connects to your project read-only. It reads your RLS settings, policies, grants and Auth settings together (never your rows), and flags tables anyone can read plus policies that look fine but aren't, like "signed in only" on an app with open sign-ups. Each finding comes with the SQL fix and a prompt to paste back into your AI builder. Built with Lovable? See the Lovable security check.
To go deeper on policies, roles and what bypasses RLS, read the complete guide to Supabase Row Level Security.
FAQ
What does RLS stand for in Supabase? Row Level Security. It's a Postgres feature that filters each table row by row, based on policies you write, so each visitor only reads or changes the rows they're allowed to.
Is RLS enabled by default in Supabase? Only for tables created in the dashboard's Table Editor. Tables created with SQL, migrations or AI builders start with RLS off unless you've installed the automatic RLS event trigger.
Is my data public if RLS is off?
If the table is in the public schema and the API roles can reach it, yes: anyone with your public key can read it, and often write to it too. The red Unrestricted badge marks exactly this.
Why does my app show no data after I enable RLS? RLS with no policies blocks everything. That's RLS working. Add a policy for the access you want, like reading your own rows, instead of turning RLS back off.
Does RLS protect my database from the service role key?
No. The secret / service_role key skips RLS by design. It belongs only in server code and must never appear in your frontend.
See which of your Supabase tables anyone can read, free
Connect your Supabase project read-only. GuardLayer checks RLS, policies, grants and functions as they are right now, and gives you the SQL to fix each problem. It never reads your rows and can't change anything.
Check my Supabase projectPrefer to check code instead? Scan your repo, no signup.