DEV Community

Cover image for Supabase "RLS disabled in public": what it means
Denys Zaichenko for Denys

Posted on Originally published at madsalt.tech

Supabase "RLS disabled in public": what it means

TL;DR. "RLS Disabled in Public" (lint 0013_rls_disabled_in_public, level ERROR) means a table in an exposed schema has row level security off while anon or authenticated can select from it. In my local test the public key read all 3 of 3 rows from such a table and inserted a new one with 201 Created. Enable RLS, add a policy per operation and check the grants, all in one migration.

I built a free Supabase security checker and this is the finding I see most in apps built with AI tools. Supabase's Security Advisor reports it as an error. Here is what it means and what it allowed when I reproduced it.

What does "RLS disabled in public" mean?

The Security Advisor runs a lint called rls_disabled_in_public. Its SQL flags a table when three things are true at once:

  1. It's an ordinary table with row level security off.
  2. anon or authenticated has SELECT on it.
  3. Its schema is in the Data API's Exposed schemas list, which is public by default.

The lint page rates it ERROR and calls full create, read, update and delete over the internet "a critically unsafe configuration". The "public" in the name is the schema, but the lint covers every exposed schema. That's why an extra schema on that list deserves a look too, and I wrote about it in Supabase exposed schemas.

What can the public key do to such a table?

I created a table orders with three fake customers on a local Supabase stack (CLI 2.109.1, Postgres 17.6, PostgREST 14.14), with the grants existing projects give new tables, and RLS off. Then I called the Data API with the publishable key, the way a browser does.

One table, the public key, RLS off and on. Local Supabase, 3 rows of fake orders. RLS off, read: Publishable key → orders, RLS off → 3 of 3 rows. RLS off, insert: Publishable key → orders, RLS off → 201 Created. RLS on, no policy yet: Publishable key → orders, RLS on → 0 rows.
My reproduction on a local Supabase stack, 23 October 2026.

Request with the publishable key RLS off RLS on, no policy
GET /rest/v1/orders?select=* 200, all 3 rows with names and emails 200, []
POST /rest/v1/orders with a new row 201 Created 401, 42501 "new row violates row-level security policy"

Anyone who opens your app can copy the publishable key from the page. With RLS off, that key is a read and write pass to the table.

How do you fix it without breaking the app?

If you only enable RLS, the table returns nothing to the app, because there's no policy yet. That empty screen is the moment when people, and AI tools, add a policy with using (true), and that hands every row back to everyone. Write the real policies in the same migration:

alter table public.orders enable row level security;

revoke all on table public.orders from anon, authenticated;
grant select, insert on table public.orders to authenticated;

create policy "Customers read their orders"
on public.orders for select
to authenticated
using ((select auth.uid()) = user_id);

create policy "Customers create their orders"
on public.orders for insert
to authenticated
with check ((select auth.uid()) = user_id);
Enter fullscreen mode Exit fullscreen mode

If a table is meant to be public, like a list of published articles, keep RLS on anyway and write one select policy for anon, with no write grants. If you never use the Data API and only talk to Postgres from your server, remove public from Exposed schemas instead. Supabase's maintainers suggested that in the same issue.

To make sure new tables never start without RLS, Supabase's event triggers guide has a trigger that enables it on every table created in public. And from 30 October 2026 new tables get no Data API grants by default, so they answer 42501 until you grant them. I covered that in Supabase 42501 permission denied.

What does a clean result not prove?

RLS being on says nothing about whether the policies are right. In the same test a policy written as using (true) gave the public key all 3 rows again, and this lint stays quiet once RLS is on. Policies that allow every row need a separate check. How to test Supabase RLS shows how to prove each policy against real users.


Originally published at madsalt.tech. Written with AI assistance, checked by me.

Top comments (0)