buildwiththis home

Supabase “RLS Disabled in Public”: what it means and the safe fix

Short answer: Supabase’s Security Advisor shows “RLS Disabled in Public” when a table in the public schema has Row Level Security off. Supabase says anyone with your project URL can then read, edit and delete every row; our test confirmed it. Fix it in one migration: enable RLS, grant each role only what it needs, and add a policy per operation.

Tested and sources checked Oct 24, 2026Fix guide, not a tool verdictRe-test due by Nov 30, 2026

What the warning means

Supabase’s lint reference lists it as 0013_rls_disabled_in_public:

  • Level: ERROR
  • Summary: “Table publicly accessible”
  • Ramification: “Anyone with your project URL can read, edit, and delete all data in this table because Row-Level Security is not enabled.”

This is what Supabase’s scan returned for our test table (read through Supabase’s MCP server, which runs the same checks as the dashboard):

name:        rls_disabled_in_public
title:       RLS Disabled in Public
level:       ERROR
description: Detects cases where row level security (RLS) has not been
             enabled on tables in schemas exposed to PostgREST
detail:      Table `public.notes` is public, but RLS has not been enabled.
remediation: https://supabase.com/docs/guides/database/database-linter?lint=0013_rls_disabled_in_public

You’ll see it in the dashboard under Advisors → Security Advisor, from the CLI with supabase db advisors, and through the MCP get_advisors tool, per Supabase’s docs. Supabase notes that some findings may be intentional, so check each against who should really have access.

Why it’s an ERROR, not a style warning

Tables in public are reachable over Supabase’s Data API, the same API supabase-js calls. Supabase’s lint page says that without RLS, anyone with the project URL can create, read, update and delete rows, and calls that “a critically unsafe configuration”. Your publishable (or older anon) key is meant to be public, so the key isn’t what protects you. RLS is (Supabase: Securing your data).

Is my data exposed right now? The logged-out check

Run this against your own project only, with the publishable key from your app’s front end. Never point it at someone else’s project.

curl "https://YOUR-PROJECT.supabase.co/rest/v1/your_table?select=*" \
  -H "apikey: YOUR-PUBLISHABLE-KEY"

BuildWithThis check. Tested Oct 24, 2026. If you get rows back while logged out, anyone can read that table.

Before our fix, this is what came back from our RLS-off test table, logged out:

Logged-out requestWhat came back
Read, publishable key200, every row in the table
Read, legacy anon key (apikey header only)200, every row. No Authorization header needed
Insert a row201, row added
Edit another user’s row200, row changed
Delete a row204, row gone

To find every table with the problem, run this in the SQL Editor:

-- Public tables with RLS off that anon or authenticated can reach
select c.relname as table_name,
       has_table_privilege('anon', c.oid, 'SELECT,INSERT,UPDATE,DELETE')
         or has_any_column_privilege('anon', c.oid, 'SELECT,INSERT,UPDATE') as anon_can_reach,
       has_table_privilege('authenticated', c.oid, 'SELECT,INSERT,UPDATE,DELETE')
         or has_any_column_privilege('authenticated', c.oid, 'SELECT,INSERT,UPDATE') as authenticated_can_reach
from pg_catalog.pg_class c
join pg_catalog.pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
  and c.relkind in ('r', 'p')
  and not c.relrowsecurity
  and (
    has_table_privilege('anon', c.oid, 'SELECT,INSERT,UPDATE,DELETE')
    or has_any_column_privilege('anon', c.oid, 'SELECT,INSERT,UPDATE')
    or has_table_privilege('authenticated', c.oid, 'SELECT,INSERT,UPDATE,DELETE')
    or has_any_column_privilege('authenticated', c.oid, 'SELECT,INSERT,UPDATE')
  )
order by 1;

BuildWithThis query (a Postgres catalog query, not a Supabase snippet). Tested Oct 24, 2026: it listed every table Security Advisor flagged, plus two it didn’t: a table logged-out visitors could only insert into (a logged-out insert returned 201) and one with a column-level select grant. It skipped tables with RLS on and an RLS-off table with no API grants. It checks public only; if you expose other schemas, change n.nspname = 'public' to n.nspname in ('public', 'your_schema'). An RLS-off table it doesn’t list isn’t reachable today, but turn RLS on anyway: one grant later and it’s open.

The fix: RLS, grants and policies in one migration

Replace your_table and user_id with your table and its owner column. This example lets each signed-in user read and change only their own rows.

-- 1. Turn on RLS
alter table public.your_table enable row level security;

-- 2. Take back the client-role grants, then grant only what's needed
revoke all on table public.your_table from anon, authenticated;
grant select, insert, update, delete on table public.your_table to authenticated;

-- 3. One policy per operation (owner-only example)
create policy "owner can read" on public.your_table
  for select to authenticated
  using ((select auth.uid()) = user_id);

create policy "owner can insert" on public.your_table
  for insert to authenticated
  with check ((select auth.uid()) = user_id);

create policy "owner can update" on public.your_table
  for update to authenticated
  using ((select auth.uid()) = user_id)
  with check ((select auth.uid()) = user_id);

create policy "owner can delete" on public.your_table
  for delete to authenticated
  using ((select auth.uid()) = user_id);

From Supabase docs (Row Level Security). Tested by BuildWithThis on Oct 24, 2026. Supabase’s docs give the select and insert policy shapes; we wrote the update and delete policies in the same pattern and confirmed all four work.

Turn RLS on, and expect your app to go quiet

Supabase says that once RLS is on, the anon role can’t read or write the table through the API until you add policies (lint 0013). That’s expected, not a bug. With RLS on but no policies yet, our test returned no errors for most requests, just nothing:

Request (RLS on, no policies)What came back
Logged-out read200, empty list []
Logged-out insert401, code 42501, “new row violates row-level security policy”
Logged-out edit200, empty list. Nothing changed
Logged-out delete204. Nothing deleted
Signed-in owner reads their own rows200, empty list. This is the “my app shows no data” moment

Take back grants you don’t need

Grants decide whether a role can reach a table; RLS decides which rows. Supabase’s RLS docs say adding policies doesn’t take grants back, and a table protected only by policies still gives anon an insert path if the grant was never revoked. So the migration revokes everything from anon and authenticated, then grants back only what the app uses. After it, our logged-out requests got 401 “permission denied for table” instead of an empty list.

One policy per operation

Write a policy for each of select, insert, update and delete, and put with check on insert and update so nobody can write a row they don’t own (Supabase). In our test, the with check on update stopped user B from handing their row to user A.

Public read-only data (like a product list)

If logged-out visitors really should read a table, grant select and nothing else, and add a read-only policy:

alter table public.your_table enable row level security;
revoke all on table public.your_table from anon, authenticated;
grant select on table public.your_table to anon, authenticated;

create policy "anyone can read" on public.your_table
  for select to anon, authenticated
  using (true);

BuildWithThis pattern built from Supabase’s RLS docs. Tested Oct 24, 2026: logged-out reads returned 200; logged-out inserts, edits and deletes returned 401 42501, and a signed-in edit returned 403 42501. Never use using (true) on an insert, update or delete policy.

Don’t “fix” it with the secret key

The service_role and secret keys bypass RLS. Supabase says they must stay server-side and are never safe to expose (RLS, Securing your data). If the errors go away because the browser now holds a secret key, the table is more open than before. Not sure which key is which? See which Supabase key goes in your front end.

Not using the Data API at all?

If your app only talks to the database from a server, Supabase says you can remove public from Exposed schemas in the API settings, so nothing in it is reachable over the API (Advisors docs). We didn’t test this.

Prove it’s fixed

1. Rescan in Security Advisor

Dashboard → Advisors → Security Advisor, then rerun. In our test the “RLS Disabled in Public” finding was gone after the migration.

2. Rerun the logged-out check

Same curl as above. After our fix, every logged-out request got 401 with code 42501, using either the publishable key or the legacy eyJ… anon key.

3. Two-account test

Sign up two test users, A and B. Signed in as B, try to read, edit and delete A’s rows. This is our own method, and it’s the step that catches loose policies. In our scanner test, “Security Advisor flagged the table with RLS off but not the policy that let any signed-in user read every profile; only the second-user test caught that.”

Watch the status codes: when RLS blocked B’s edit and delete, the API still answered 200 and 204. Check whether the data changed, not just whether the request “worked”.

After the fixWhat came back
Logged out: read, insert, edit, delete (publishable key or legacy anon key)401, code 42501, “permission denied for table”
User A reads, no filter200, only A’s rows
User B asks for A’s rows200, empty list
User B edits A’s row200, empty list. A’s row unchanged
User B deletes A’s row204. A’s row still there
User B inserts a row owned by A403, code 42501, “new row violates row-level security policy”
User B moves own row to A (changes user_id)403, code 42501. The with check blocked it
User A inserts, edits and deletes own rows201, 200, 204. All worked
Security Advisor rescan“RLS Disabled in Public” gone for the table

4. Make it stick with a pgTAP test

Supabase’s answer is a database test that asserts allow and deny for each role and operation, run with the CLI against your local database (Testing your database). Supabase’s RLS docs put it plainly: until the suite passes, you don’t know whether the policies do what you intended.

supabase test new your_table_rls.test
supabase test db

Here’s the test we wrote for our table. Change the table and column names to match yours.

begin;
create extension if not exists pgtap with schema extensions;
select plan(8);

-- Two fake users and one row each (rolled back at the end)
insert into auth.users (id, email) values
  ('11111111-1111-1111-1111-111111111111', 'test-user-a'),
  ('22222222-2222-2222-2222-222222222222', 'test-user-b');
insert into public.notes (user_id, body) values
  ('11111111-1111-1111-1111-111111111111', 'A note'),
  ('22222222-2222-2222-2222-222222222222', 'B note');

-- Logged out (anon)
set local role anon;
select throws_ok('select * from public.notes', '42501', null, 'logged out: cannot read');
select throws_ok($$insert into public.notes (user_id, body) values ('11111111-1111-1111-1111-111111111111', 'x')$$, '42501', null, 'logged out: cannot insert');

-- Signed in as user A
set local role authenticated;
set local request.jwt.claims = '{"sub": "11111111-1111-1111-1111-111111111111", "role": "authenticated"}';
select ok(exists(select 1 from public.notes where body = 'A note'), 'A can read own row');
select is((select count(*)::int from public.notes where user_id = '22222222-2222-2222-2222-222222222222'), 0, 'A cannot read B rows');
select throws_ok($$insert into public.notes (user_id, body) values ('22222222-2222-2222-2222-222222222222', 'x')$$, '42501', null, 'A cannot insert a row owned by B');
with u as (update public.notes set body = 'changed' where user_id = '22222222-2222-2222-2222-222222222222' returning 1)
  select is(count(*)::int, 0, 'A cannot update B rows') from u;
with d as (delete from public.notes where user_id = '22222222-2222-2222-2222-222222222222' returning 1)
  select is(count(*)::int, 0, 'A cannot delete B rows') from d;
with u as (update public.notes set body = 'edited' where body = 'A note' returning 1)
  select is(count(*)::int, 1, 'A can update own row') from u;

select * from finish();
rollback;

Written by BuildWithThis in the shape of Supabase’s example. Tested Oct 24, 2026: all 8 checks passed against our test project. We ran the file’s SQL directly on the project, inside its rolled-back transaction, not through supabase test db, which needs the CLI’s local stack. A copy with one expectation deliberately wrong reported “Looks like you failed 1 test of 8”, so failures do show.

The same warning on tables you didn’t create (spatial_ref_sys)

If you installed PostGIS in public, Security Advisor reports “RLS Disabled in Public” on public.spatial_ref_sys. Supabase’s knowledge base article says it isn’t a false positive. You can’t enable RLS on it because supabase_admin owns it (that errors with 42501). The fix is moving PostGIS to the extensions schema, and if your tables already have PostGIS columns, Supabase says to ask its support team to do it. We didn’t test this.

Other Advisor findings to fix while you’re there

  • 0007 policy exists rls disabled and 0008 rls enabled no policy: see the lint reference.
  • 0010 security definer view: views bypass RLS by default. In Postgres 15 and later, create them with security_invoker = true so they obey the underlying tables’ policies (RLS docs).
  • 0015 rls references user metadata: signed-in users can edit their own user_metadata, so policies that rely on it can be bypassed (RLS docs).
  • 0023 sensitive columns exposed (“Sensitive data publicly accessible”): an RLS-off table with column names like password, token or api_key (lint reference).
  • 0024 permissive rls policy: see the lint reference.

What scanners don’t catch

Security Advisor was our pick of 6 free scanners (76/100). In that Oct 16 test it caught our table with RLS off, but no tool caught most of the 10 security holes we planted; the best single tool caught 4.5. A clean Advisor scan means RLS is on, not that your policies are right. Run the free vibe-coding security checklist for the checks scanners miss.

New tables throwing 42501 instead? That’s the opposite problem, covered in our 42501 fix guide. Choosing a database first? See how Supabase compared to Neon and D1.

What we tested

On Oct 24, 2026, about 4 AM ET, on a new Supabase project made for this test (Free plan, Postgres 17.11, ca-central-1). The project still had Supabase’s pre-October-30 defaults, so new tables were exposed to the API as soon as they were created. We made a notes table with SQL, left RLS off, and scanned it. Then we sent requests to the REST API (what supabase-js calls) logged out and as two test users, applied the fix step by step, and scanned again. We also tested the public read-only pattern, then switched the project to the post-October-30 defaults with Supabase’s opt-in SQL and made an RLS-off table with no API grants. The project was paused afterwards.

  • RLS off, no API grants: Security Advisor didn’t flag it, and logged-out and signed-in reads got 42501.
  • The same table after running the grant from the 42501 hint (grant select … to anon): Advisor flagged it as “RLS Disabled in Public”, and a logged-out read returned the row.
  • The “find every table” query (later the same morning, on the same project, with six empty test tables): it listed the three tables Advisor flagged and two Advisor missed (insert-only for anon, and a column-level select grant), and skipped the no-grant and RLS-on tables. A logged-out insert into the insert-only table returned 201. We dropped the test tables afterwards.

Not tested: tables created in the Dashboard’s Table Editor, the dashboard’s Advisor screen itself (we read the same lints through Supabase’s MCP server), the supabase db advisors CLI command, supabase test db, PostGIS, removing public from Exposed schemas, and whether any AI tool turns RLS on by itself. We don’t make claims about Lovable, Bolt or Cursor here.

Tested on: Oct 24, 2026 · Supabase Free plan · Postgres 17.11 · pre-Oct-30 defaults (plus one post-Oct-30 table). SQL from Supabase docs unless labelled BuildWithThis. Re-test due Nov 30, 2026. AI agents ran this test and wrote this page; the raw results are in our test log.

Questions people ask

What does “RLS Disabled in Public” mean in Supabase?

A table in your public schema has Row Level Security turned off. Supabase’s docs rate it ERROR, summarise it as “Table publicly accessible”, and say anyone with your project URL can read, edit and delete all data in that table. In our test, a logged-out request with the public key did all four.

Is my data actually exposed, or is it just a warning?

Usually exposed. In our test, a logged-out curl with only the publishable key read every row, added a row, changed another user’s row and deleted a row. One exception we saw: a table with RLS off but no API grants (the default for new tables from October 30, 2026) wasn’t flagged and returned 42501. It got flagged, and became readable, the moment we granted select to anon. Run the logged-out check on your own project to be sure.

My anon/publishable key is in my front-end code. Is that the problem?

No. Supabase says the publishable key, and the older anon key, are safe to expose when RLS is on: they identify your project, they aren’t secrets. RLS is what protects the data. The key that must never reach the browser is the service_role or secret key.

I enabled RLS and now my app shows no data. What happened?

That’s expected. With RLS on and no policies, Supabase returns no rows. In our test even the signed-in owner got an empty list, with no error. Add a policy for each operation your app needs, and grant those operations to the authenticated role.

Can I just add a policy that allows everything?

Only for data that really is public and read-only, like a product list: grant select (and nothing else) and add a select policy. In our test that let logged-out visitors read while inserts, edits and deletes were refused. Never use an allow-everything policy for writes.

Should I use the service_role key so the errors go away?

No. Supabase says the service_role and secret keys bypass RLS and must stay on the server. In a browser, anyone can copy them and read or change everything.

Why does Supabase flag spatial_ref_sys? I didn’t create it.

It comes from the PostGIS extension when PostGIS is installed in the public schema. Supabase’s knowledge base says these aren’t false positives, and you can’t enable RLS on it yourself because supabase_admin owns it (you get error 42501). The fix is moving PostGIS to the extensions schema; if your tables already have PostGIS columns, Supabase says to ask its support team to move it. We didn’t test this.

What’s “Sensitive data publicly accessible”?

A related Supabase lint (0023, also ERROR). It flags exposed tables with RLS off whose column names look sensitive, such as password, token, api_key, ssn or credit_card. Fix it the same way.

I created the table in the Dashboard. Why is RLS off?

Supabase says tables created in the Dashboard’s Table Editor have RLS on by default, while tables created in the SQL Editor or by other tools need it turned on explicitly. If an AI tool, a migration or a SQL script made the table, that’s the likely reason. We tested a table made with SQL (RLS was off); we didn’t test the Table Editor.

How do I check RLS is working?

Four ways, all on your own project: rescan in Security Advisor; run the logged-out curl from the “Is my data exposed right now?” section above and expect a 401 or an empty list; sign in as a second test user and try to read, edit and delete the first user’s rows; and keep a pgTAP test that runs with supabase test db. In our test, updates and deletes that RLS blocked returned success codes with nothing changed, so check the data, not just the status code.

Is Supabase secure?

It can be. Supabase’s own docs put the responsibility on your settings: every exposed table needs RLS, tight grants and correct policies, and the secret key stays on the server. Security Advisor catches RLS that’s off, but in our scanner test it missed a policy that let any signed-in user read every profile. Use the free checklist for the rest.

Does this relate to the 42501 “permission denied” error?

It’s a different problem. RLS Disabled in Public means a table is too open. Error 42501 “permission denied for table” usually means a table has no API grants, which is the default for new tables from October 30, 2026. Don’t fix 42501 by pasting the grant from the error hint on a table with RLS off: in our test, that made the table readable by anyone and brought the RLS warning back.

Sources

Related: Supabase 42501 fix guide · publishable vs anon key · Vibe coding hub

General guidance from one small test project, not a security audit. Check only projects you own.