Security basics
Supabase "RLS disabled in public": what the warning misses
Supabase reports "RLS disabled in public" as an error. It says nothing about the read policy that leaves your table just as open to strangers.

In short
- "RLS disabled in public" means one thing: a table in your public schema has row level security switched off, so anyone holding your project address and publishable key can read it.
- It says nothing about a table where you switched the setting on and then wrote a policy that lets everyone read. The advisor has a rule for always-true policies and that rule skips read policies on purpose.
- Clear the flagged tables, then check the rest from outside, because the advisor reads your settings and never asks your database what a stranger actually gets back.
You opened the Security Advisor in your Supabase dashboard, or something pasted its output at you, and there it is in red: RLS disabled in public. Under it, one line per table, in Supabase's own words:
Table public.profiles is public, but RLS has not been enabled.
Here is the part that guide after guide gets wrong: clearing that list does not mean nobody can read your data. The advisor has a separate rule for a policy that lets everyone in, and that rule skips the exact shape an AI builder writes when it unbreaks your app. So the list you just fixed and the list of tables a stranger can read are two different lists, and one of them is not printed anywhere in your dashboard.
What does "RLS disabled in public" mean?
One table in your public schema has row level security switched off, and the consequence is that anyone holding your project address can read every row in it.
Both halves of that need unpacking. The public schema is the default place a table goes when nobody says otherwise, and it is the part of your database Supabase publishes on the web: every project answers requests at an address of its own, and the key needed to talk to it is in the code your site sends to every visitor. Row level security is the switch that decides whether your rules get consulted before rows are handed over. With it off, there is nothing to consult, so the answer is always yes.
That combination is why this one is reported as an error rather than a warning. The advisor grades its findings, and this is the top of the scale.
Think of the advisor as an inspector with a clipboard. It reads your paperwork carefully and it is good at it. It never tries the handle. Nothing in that panel is the result of a request anybody made to your database.
Will enabling RLS break my app?
Yes, straight away, and that is the setting working.
The fix Supabase gives you is one line, and you run it in the SQL Editor:
alter table public.profiles enable row level security;
Supabase's own documentation is direct about what happens next: data becomes inaccessible through the API using a publishable key until policies are defined. So your lists come back empty, your screens go blank, and the error in the advisor is replaced by a quieter entry saying the table has RLS enabled but no policies exist.
That is the door shut with nobody on the list yet. The next thing most people do is add a policy letting everyone read, because that is what makes the screens come back.
The failure the warning is not looking for
A table with row level security on, and a read policy whose condition is
USING (true), hands over exactly the same rows to exactly the same stranger.
The advisor does not flag it.
That is not an oversight. Supabase does have a rule for always-true policies, and it leaves read policies out of that rule on purpose. Their own description of it says so:
SELECT policies with `USING (true)` are intentionally excluded as this
pattern is often used deliberately for public read access.
The rule is right about the general case. A product catalogue, a list of published articles, a map of venues: those are meant to be readable by anyone, and flagging them would train every developer on the platform to ignore the panel. What no linter can know is whether the table it is looking at holds venues or customers.
And a policy allowing everyone to read is the fastest way to make a broken app
work again, which is why an AI builder reaches for it. Ask Cursor or Lovable to
fix the empty screens and FOR SELECT USING (true) is a common answer. Your app
loads, the error clears, the panel goes quiet, and the table is as readable as
it was before you started.
| What the advisor can see | How it reports it | What a stranger with your publishable key gets |
|---|---|---|
| RLS switched off | Error | Every row |
| RLS on, no policies at all | Info | Nothing |
RLS on, FOR SELECT USING (true) | Nothing at all | Every row |
RLS on, FOR ALL USING (true) | Warning | Every row, and can change them |
RLS on, USING (auth.uid() = user_id) | Nothing at all | Only their own |
The two rows that report nothing at all are the pair worth sitting with. One of them is a table nobody outside your app can touch. The other is a table anybody can read. Your dashboard is equally quiet about both.
How that policy came to be written, and what to replace it with table by table, is the subject of Row Level Security is on and your table is still public. If the same policy also has to let your app save data, the error you meet next is new row violates row-level security policy.
Why the table you made with a migration never warned you
Because the Table Editor switches row level security on for you, and SQL does not.
Supabase documents the split plainly: tables created with the dashboard's Table
Editor have RLS enabled by default, and tables created with raw SQL have to have
it enabled explicitly. A table you clicked into being starts protected. A table
that arrived through a migration file, a supabase db push, a snippet in the
SQL Editor, or a statement your AI builder ran on your behalf starts open.
That second route is how an AI builder makes a table. It writes the SQL and runs it for you, so you never saw the checkbox and never saw it unticked.
The habit that closes it is putting the line in the migration next to the thing it protects:
create table public.profiles (
id uuid primary key references auth.users,
full_name text
);
alter table public.profiles enable row level security;
How to check the tables the advisor cleared
Ask your database the question a stranger asks: send a request from outside, using the publishable key that ships in your app, and see what comes back.
This is the difference the whole article turns on. The advisor reads your configuration. A request reads your rows. Those are two different questions, and a table can pass the first while failing the second, which is exactly what a permissive read policy does.
We ran that request at scale. Between 12 and 14 August 2026 we ran nine external checks over 30,998 live apps published from Lovable, Base44, Replit, v0 and Bolt. Of the 3,680 Supabase-backed apps where the check could complete, 2,096 had at least one table that answered an anonymous request with rows. That is 57%, and it is a share of the apps we could get a straight answer from rather than of everything we scanned. Among Bolt-built apps the figure was 27 of 35, which is a small enough sample to read as a direction rather than a rate. The full dataset is published, and what the 57% is a share of walks through the counting.
You can run the same request yourself against one table with a browser and your own publishable key. If you would rather not do it table by table, our free scan asks your live app from outside and tells you which tables answered. It takes about 20 seconds and needs no account: scan your app.
Do I need a backup before I change RLS policies?
For any table your app writes to, yes. For a read-only table you are only tightening, the risk is your app going blank rather than your data going missing.
Two different things are worth separating here, because only one of them is about the repair.
What a permissive policy already allowed. If the rule on the table was
FOR ALL USING (true) rather than FOR SELECT, then anyone who found it could
change and delete rows as well as read them, and tightening the policy today
does nothing about yesterday. That version of this usually surfaces as a support
message about data that changed on its own, or as a table that is suddenly
empty.
The repair itself. Rewriting policies across a dozen tables is a change to a live database, written by the same tools that produced the problem. A migration that drops a policy and recreates it wrongly is an ordinary Tuesday, and the way back is a copy of how things were an hour ago.
On a paid Supabase plan you have last night's copy sitting in the console. On the free plan there is nothing to fall back to, because the free plan takes no automatic backups at all. If that is you, take one before you touch a policy: how to back up a Supabase database on the free tier is the ten-minute version.
What Reeve Care keeps a copy of
Your Supabase database, copied on a schedule, held outside your Supabase account, encrypted, and read back before the date on your dashboard moves. The files your users uploaded travel with it once you connect a Storage key.
Two limits, said up front. Backups are Supabase only: if your data lives somewhere else we say so rather than sell you a subscription that watches an empty box. And the Storage key is asked for separately, because Supabase issues no read-only key for files, so the one that copies your uploads can also write. The key that copies your database cannot. Connecting it is optional, and the database is backed up either way.
The restore is the part that matters for this article. Putting an old copy back over a live database is the scariest button in the product, so Care takes a copy of the current state first, and only then replays the one you picked. The restore has an undo of its own.
Care's other half is the one this article keeps pointing at. A policy that loosened during a migration is not a thing you find by looking, so the same external check runs again on a schedule and tells you when the answer changes. Uptime, a monthly report and the scan sit on the same subscription.
What a copy does not do is write your policies for you, and no backup makes an open table closed. Those tables are still yours to fix. What the copy changes is what happens when the fix goes sideways. What Reeve backs up on Supabase, how often, and what a restore does draws the whole cycle, and the plans and their prices are on the pricing page.
What to do this week
What to do
- Clear the "RLS disabled in public" entries first. They are the tables where nothing is being consulted at all, and the fix is one
alter tableline each. - Then open Authentication → Policies and read the condition on every policy that survived. A
USING (true)on a read policy is invisible to the advisor and wide open to a stranger. - Decide table by table whether you would be comfortable publishing its contents on a page. That is the question the linter cannot answer for you, and it is the only one that matters for a permissive read policy.
- Put
alter table ... enable row level security;into every migration that creates a table, and re-run the advisor after each one. The dashboard's default only applies to tables you make by clicking. - Take a copy of your database before you rewrite policies on a live table, and check which Supabase plan you are on so you know whether you already have one.
Start with the table that would embarrass you most as a public page. The 10-minute security checklist covers this alongside the rest of what is worth confirming in a newly launched app, and the Supabase safety guide goes through what else tends to be left open.
FAQ
Is "RLS disabled in public" an error or a warning?
An error, and it is the highest level the advisor uses. The entry reads "Table public.<name> is public, but RLS has not been enabled." The two related entries are quieter: a table with the setting on and no policies at all is reported as INFO, and a policy whose condition is always true is reported as WARN. Those levels describe how confident the linter is about what it can see in your configuration, not how much trouble you are in.
Will enabling Row Level Security break my app?
Immediately, yes, and that is the setting doing its job. Supabase documents that data becomes inaccessible through the API with a publishable key until policies are defined, so the moment you run the enable line your lists come back empty and your screens go blank. The app comes back when you add a policy saying who may see which rows. The mistake to avoid is adding one that allows everybody, because that version also makes the app work.
I enabled RLS and now nothing loads. What happened?
Nothing broke. With row level security on and no policies written, Postgres (the database engine underneath Supabase) refuses every request, including the ones from your own app, and the advisor swaps its error for an INFO entry saying the table has RLS enabled but no policies exist. Write a policy for the rows your app is supposed to show, starting with one that compares the signed-in visitor to the owner column on the row.
My table is not flagged but anyone can read it. Why?
Most likely because row level security is on and there is a read policy whose condition is USING (true). The advisor does have a rule for always-true policies, and it deliberately skips read policies, because public read access is a reasonable thing to want for a product catalogue or a list of published articles. Nothing in your dashboard knows whether your table holds venues or customers, so that one is yours to check.
Do I need Row Level Security if my app only talks to my own server?
If the browser genuinely never talks to Supabase, and no publishable key is in the code your site sends to visitors, then the Data API is not a way in and policies are not the thing protecting those tables. That is rare in an app built with Lovable, Bolt or v0, because those builders wire the browser straight to Supabase by default. Open your own site, check whether your project URL and publishable key are in the page, and let that answer decide it.