Supabase RLS Disabled: Checks Before You Enable It
A red Security Advisor warning makes Enable RLS look like the obvious first step. On a live application, that click can block reads and writes together. Leaving it for later preserves the exposure. Inventory the table’s purpose, current privileges, and existing policies, then apply the required changes as one tested release.
Check four things before touching the switch
- Is the table exposed through the Data API?
- Which
GRANTprivileges do theanonandauthenticatedroles hold? - Does an existing policy open access broadly with a rule such as
using (true)? - Is the browser using a secret or service-role key instead of a publishable key?
RLS disabled in public means row-level security is off for a table in the public schema. A GRANT controls which operations a role may attempt on the table. RLS narrows those operations to permitted rows. Neither layer replaces the other.
RLS does not hide columns. If the same row contains email addresses, internal notes, or other fields that must not reach a browser, do not grant unrestricted table-wide SELECT. Use column privileges, a PostgreSQL 15 or later security_invoker view that exposes only the required columns, or a separate server API.
Do not infer Supabase’s 2026 defaults from the project creation date alone. The new defaults began rolling out to new projects on May 30 and are scheduled to reach existing projects on October 30. The change affects the default GRANT privileges on tables created in the future; it does not automatically revoke privileges on existing tables. Check the project setting and the current privileges directly.
1. Inspect privileges and every existing policy
Open Database → Security Advisor, identify the affected table, and decide whether it should be readable before login, writable only by signed-in users, or server-only. In SQL Editor, list the current policies:
select policyname, permissive, roles, cmd, qual, with_check
from pg_policies
where schemaname = 'public' and tablename = 'profiles';
Adding a new policy does not automatically make access stricter. Multiple permissive policies—the default type—combine with OR. One old using (true) or to public policy can therefore open access beyond a new per-user policy. Remove or narrow unnecessary policies before adding another one. If a policy with the intended name already exists, inspect its definition; use ALTER POLICY or a reviewed DROP POLICY migration instead of rerunning CREATE POLICY blindly.
2. Prepare RLS, grants, and policies together
This example assumes profiles.user_id stores the signed-in user’s ID. It allows an authenticated user to select, insert, update, and delete that user’s own rows. Adapt the table, columns, and rules to the product, then run it on staging first.
begin;
alter table public.profiles enable row level security;
grant select, insert, update, delete
on table public.profiles to authenticated;
create policy "Users can view own rows"
on public.profiles for select
to authenticated
using ( (select auth.uid()) = user_id );
create policy "Users can insert own rows"
on public.profiles for insert
to authenticated
with check ( (select auth.uid()) = user_id );
create policy "Users can update own rows"
on public.profiles for update
to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );
create policy "Users can delete own rows"
on public.profiles for delete
to authenticated
using ( (select auth.uid()) = user_id );
commit;
For an update, using decides whether the existing row may be changed and with check validates the row after the change. PostgreSQL reuses the using condition when with check is omitted, but writing both makes the intended rule easier to review. UPDATE also needs a SELECT policy that allows the row to be seen.
For a server-only table, do not invent a client policy. Revoke the Data API roles instead, for example with revoke all on table public.profiles from anon, authenticated;. For a table the application needs, grant only the required operations explicitly. If an INSERT uses a sequence directly, grant the minimum required sequence privilege separately. For data available before login, create a dedicated anon policy and review public rows and public columns independently.
Tables created in the Table Editor have RLS enabled by default. Tables created through raw SQL or SQL Editor require you to enable it. If the UI becomes empty after you turn RLS on, the rows were probably not deleted; requests are often being denied because no applicable policy or GRANT exists.
3. Migrate every key consumer before disabling the legacy key
Browsers should use a publishable key (sb_publishable_...). An existing project’s anon key is also intended for public use, but migrating to the new key system is preferable. A secret key (sb_secret_...) and the legacy service_role bypass RLS and must stay off clients.
Creating a new key does not revoke the old one. Migrate in this order:
- Replace
anonwith a publishable key in web, mobile, desktop, and already deployed clients. - Replace
service_rolewith a secret key in servers, Edge Functions, workers, CI/CD, cron jobs, external integrations, Database Webhooks, andpg_net. - Check whether any integration must send the new secret key in the
apikeyheader rather thanAuthorization: Bearer. - Confirm that no requests still use the old credentials, then disable the legacy
anonandservice_rolekeys underSettings → API Keys.
If an sb_secret_... value leaked, create a replacement, update the server, and revoke the old key. Treat an exposed legacy service_role the same way: do not patch one code path and stop. Complete the migration and disable the legacy credential. Old app releases, scheduled jobs, and webhooks can fail during the cutover, so keep a consumer inventory.
4. Test with the real roles and two users
In SQL Editor, select auth.uid(); normally returns null because there is no user JWT. SET LOCAL lasts only for the current transaction, so place it between BEGIN and ROLLBACK. In the code below, the bracketed text means the UUID of the user you want to test:
begin;
set local role authenticated;
set local request.jwt.claim.sub = '<UUID of the user under test>';
select * from public.profiles;
rollback;
That SQL is a quick policy check, not the final test. In the application, run select, insert, update, and delete while signed out, as user A, and as user B. Confirm that user A is denied even when sending user B’s ID directly, and that restricted columns never appear in the response.
Coordinate policies, required GRANT privileges, and application deployment as one short production change. If you discover a dangerously broad policy, do not keep it open for convenience. Raise its priority, validate the fix on staging, and prepare rollback SQL before closing it.