Supabase Advisor · ERROR
RLS Disabled in Public — what the Supabase advisor is telling you
Luca Urti
RLS Disabled in Public
What does “RLS Disabled in Public” mean and how bad is it?
It means a table in the public schema has Row Level Security switched off, and every table in that schema is reachable through PostgREST using the anon key — which is published in your client bundle by design and is not a secret. With RLS off there is nothing between that key and the table's contents, so anyone who views the source of your site can read the whole table, and can write to it too if the role holds the matching grant. This is the advisor's most severe class of finding for a reason: it requires no exploit, no vulnerability and no skill — only a URL, a key that is printed on the page, and one HTTP request.
Why the anon key being public is fine and this is not
The anon key is meant to be public. It identifies the project and the anon role, not a person, and hiding it would be pointless because the browser has to send it. Rotating it changes nothing about this finding. What makes a project safe is not the secrecy of that key but the policies behind it — RLS is the access control, and with RLS off the key is a full read credential for the table.
This is also why “but the table is not referenced anywhere in my frontend” is not a defence. PostgREST exposes the schema, not your code paths. A table nothing links to answers a request exactly as readily as one your homepage queries.
Enabling RLS breaks your app, and that is the correct order
enable row level security with no policies denies everything to anon and authenticated — reads that worked a minute ago now return an empty array rather than an error, which is the confusing part. Write the policies in the same migration, deploy them together, and check the list of tables afterwards rather than table by table.
Do not skip tables that look harmless. Waitlist and contact-form tables are the ones most often left open, they almost always hold email addresses, and an open one is both a data breach and a GDPR incident regardless of how simple the table is.
The fix
find every unprotected table, then close them
-- Every table in public with RLS off. Run this before and after the migration. select tablename from pg_tables where schemaname = 'public' and rowsecurity = false order by tablename; -- Per table: enable, then immediately give the app back what it legitimately needs. alter table public.waitlist enable row level security; create policy "anyone may join the waitlist" on public.waitlist for insert to anon, authenticated with check (true); -- Note there is deliberately no SELECT policy: the form writes, nobody reads -- through the API. Insert-only is a real and common shape, and it is the one -- people replace with "for all using (true)" when the dashboard looks empty.
The fix that works and costs you the database
create policy "enable read access for all users" on public.x for select using (true); is the snippet that circulates most widely, and it satisfies the advisor: the warning disappears, RLS reads as enabled, and the table is exactly as public as it was before. For genuinely public data — published blog posts, a price list — it is correct. For anything belonging to a user it is the same breach with a green checkmark on top, which is worse than the original because nothing flags it again.
Whether the trap is already in your repo is a question you can answer
Sentris reads the SQL and the client code, so it reports the shortcut above where it was actually taken — a service_role key in a browser bundle, a policy that is using (true), a table with RLS switched off. A scan needs no account and no card. How often it is wrong is measured and published.