---
title: "Supabase \"RLS disabled in public\": what it means"
description: "What the Supabase Advisor error rls_disabled_in_public means, what the public key did to such a table in my local test, and how to fix it safely."
url: "https://madsalt.tech/notes/supabase-rls-disabled-in-public/"
author: "Denys Zaichenko"
author_url: "https://madsalt.tech/about/"
language: "en"
published: 2026-10-28
---

# 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, in one migration.

I built a free [Supabase security checker](https://madsalt.tech/tools/supabase-security-check/), and its first rule, "RLS disabled on exposed tables", is the same finding Supabase's Security Advisor reports as an error. It is the most common hole I find in apps built with AI tools, so 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](https://github.com/supabase/splinter/issues/82) flags a table when three things are true at once:

1. It is 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](https://supabase.github.io/splinter/0013_rls_disabled_in_public/) 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, which is why an extra schema on that list deserves a look too; I wrote about that in [Supabase exposed schemas](https://madsalt.tech/notes/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, as 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.](https://madsalt.tech/charts/en/supabase-rls-disabled-in-public.svg)

*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, the key is a read and write pass to that table.

## How do you fix it without breaking the app?

Enabling RLS alone makes the table return nothing to the app, because there is no policy yet. That empty screen is when people, and AI tools, add a policy with `using (true)`, which hands every row back to everyone. Why updates and deletes then report success while changing nothing is in [how to enable RLS in Supabase](https://madsalt.tech/blog/how-to-enable-rls-in-supabase/). Write the real policies in the same migration:

```sql
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);
```

If a table is meant to be public, such as 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 talk to Postgres only from your server, remove `public` from Exposed schemas instead, as Supabase's maintainers suggested in the [same issue](https://github.com/supabase/splinter/issues/82).

To make sure new tables never start without RLS, Supabase's [event triggers guide](https://supabase.com/docs/guides/database/postgres/event-triggers) 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](https://madsalt.tech/notes/supabase-42501-permission-denied/).

## What does a clean result not prove?

That RLS is 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. The checker has a separate rule for that, "Policies that allow every row", and [how to test Supabase RLS](https://madsalt.tech/blog/how-to-test-supabase-rls/) shows how to prove each policy against real users.
