Supabase says “RLS disabled in public”: what it means and how to fix it
What the warning means, what's exposed until you fix it, and the SQL to turn row-level security on and write policies that hold.
By Sameem Amjad
The short answer
The warning means a table in your public schema can be read and changed by anyone who has your project's anon key. The anon key is in your app's JavaScript, so that means anyone who opens your site. Turn row-level security (RLS) on for the table, then add policies that say exactly who may read and write which rows.
Why it matters
Supabase exposes the tables in your public schema through an auto-generated REST API. Your front end calls that API with the anon key, which is public by design: it ships to every visitor's browser. Row-level security is what stops that key from being a master key. With RLS off, a request like this returns every row in the table, and similar requests can change or delete them:
curl 'https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*' \
-H "apikey: YOUR_ANON_KEY" \
-H "Authorization: Bearer YOUR_ANON_KEY"This is the most common serious problem in apps built with AI tools. In 2025 it got its own CVE (CVE-2025-48757) after security researchers found apps generated by Lovable leaving tables open this way.
Step 1: turn RLS on
In the SQL editor, enable RLS on every table the warning lists:
alter table public.profiles enable row level security;You can also do it in the dashboard: Table Editor, open the table, enable RLS. Once RLS is on and the table has no policies, the anon and authenticated roles can't see any rows. Reads return an empty array rather than an error, so your app will look empty until you add policies. The service_role key bypasses RLS entirely, which is why it must never be in front-end code.
Step 2: add policies for who can do what
Policies are SQL rules attached to a table. using decides which existing rows a user can see, update or delete; with check decides which new or changed rows they may write. For a table where each user owns their own rows:
-- Signed-in users can read their own profile
create policy "Users can read their own profile"
on public.profiles for select
to authenticated
using ( (select auth.uid()) = user_id );
-- Users can create a profile only for themselves
create policy "Users can create their own profile"
on public.profiles for insert
to authenticated
with check ( (select auth.uid()) = user_id );
-- Users can update only their own profile, and can't hand it to someone else
create policy "Users can update their own profile"
on public.profiles for update
to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );If some data really is public, say so explicitly. A products table everyone can read, with no write policy, so only your server can change it:
create policy "Products are readable by everyone"
on public.products for select
to anon, authenticated
using ( true );Policies filter rows; they don't grant access to the table itself. On Supabase projects created since 30 May 2026, and on all projects from 30 October 2026, new tables in public no longer get default privileges for anon and authenticated. If a request fails with permission denied (error 42501) instead of returning rows, grant the table the access your policies describe:
grant select on public.products to anon, authenticated;
grant select, insert, update on public.profiles to authenticated;Two details matter for speed. Wrapping auth.uid() in a select, as above, lets Postgres evaluate it once per query instead of once per row, which is what Supabase recommends. And the column you compare against should be indexed:
create index on public.profiles (user_id);Step 3: check the places RLS doesn't cover
- Views. A view runs with its owner's permissions by default, so it can skip the table's RLS. Create views with
security_invoker = true(Postgres 15 and later) so they respect the caller's policies. - Functions marked
security definerrun as their owner and bypass RLS. Keep them out of the exposed schema, or check permissions inside them. - Storage. Files in Supabase Storage have their own policies on
storage.objects, and a public bucket is readable by anyone with a file's URL. - The
service_rolekey. If it's anywhere in your front-end code or a public repo, rotate it now: it bypasses every policy.
create view public.my_orders
with (security_invoker = true) as
select * from public.orders;Step 4: test it like an attacker
Run the request from earlier again, with the anon key and no user session. For a private table you should now get an empty array (or a permission denied error, if the table has no grant for anon), and an insert should fail. Then sign in as an ordinary user and confirm you can see your own rows and nobody else's. Finally, open the Security Advisor in the Supabase dashboard and check the warning has gone.
When the tables aren't the only problem
If an AI tool generated your app, open tables are rarely the only issue. The same apps tend to have secrets in client code, unchecked inputs and payment webhooks that trust the browser. If you'd rather have the whole app checked properly, that's what my app rescue audit covers.