Supabase Row Level Security: How RLS, Grants, and Policies Work
Supabase Row Level Security (RLS) is one part of your Data API authorization model, not the only gate. A request must first reach an exposed schema and pass Postgres grants; RLS then decides which rows the active role may read or change. If RLS is disabled, granted roles can access rows without policy filtering—but a missing grant still blocks the request.
Correction note — October 23, 2026: this guide now separates grants from RLS, corrects USING/WITH CHECK behavior, removes unverified product claims, and covers Supabase's October 30 default-grant change. The original publication date is unchanged.
The three questions every Data API request must pass
When a browser, mobile app, or server calls Supabase's auto-generated REST or GraphQL API, check these layers in order:
- Is the schema exposed through the Data API?
publicis commonly exposed; a dedicated API schema can make the intended surface easier to audit. - Does the active Postgres role have the required privilege? A signed-out request normally uses
anon; a signed-in Supabase Auth user normally usesauthenticated.GRANTcontrols whether the role can reach the table and operation. - Does an RLS policy allow the row? If RLS is enabled, policies filter the rows the role may select, insert, update, or delete.
Supabase documents grants and RLS as separate layers. Postgres evaluates the table privilege before applying the row policy. A missing grant commonly returns a 42501 permission denied error; a valid grant with no matching RLS policy commonly returns no rows or rejects the write.
That distinction matters when an AI coding tool creates a migration. Adding RLS without the required grant can break the app. Adding broad grants without RLS can expose every row the granted operation can reach. Review both in the same migration.
Is the publishable key safe in frontend code?
Yes. Supabase publishable keys—and the legacy anon key—are designed for public components such as browser and mobile apps. They identify the project and help select the Postgres role; they are not secrets that authorize unrestricted database access.
The server-only secret key is different. Requests authorized as service_role can bypass RLS, so secret keys and legacy service_role keys must not ship in a browser bundle, mobile binary, public repository, or client-readable environment variable. Supabase recommends its newer publishable and secret keys for new work.
Does Supabase enable RLS automatically?
Tables created through the Supabase Dashboard have RLS enabled by default. Tables created with raw SQL, migrations, the CLI, the Management API, MCP, or an AI coding tool need an explicit statement unless the creation flow adds one:
alter table public.profiles enable row level security;RLS enabled with no applicable policy is default deny for ordinary roles subject to RLS. That is safe from a row-access perspective, but it can break the feature until you add the policies the application needs. Table owners normally bypass RLS unless FORCE ROW LEVEL SECURITY is enabled, so testing only through an owner connection does not reproduce client behavior.
October 30 changes the default grants for new tables
Supabase announced this change on April 28, 2026; it is not a new incident. The time-sensitive part is the rollout deadline: on October 30, 2026, existing hosted projects move to opt-in Data API grants for newly created tables in public.
After the rollout:
- existing tables keep their current grants;
- a new
publictable needs an explicitGRANTbefore the Data API, GraphQL, orsupabase-jscan reach it; - direct Postgres connections through an ORM,
psql, or an application server are unaffected; - RLS behavior does not change.
If a generated migration creates a table after the change, bundle the minimum grants with RLS and the policies. Do not respond to a 42501 error by disabling RLS or granting every operation to every client role.
For example, if signed-out visitors should not reach profiles and signed-in users need reads and limited updates:
grant select, update on table public.profiles to authenticated;
alter table public.profiles enable row level security;
create policy "users can read their profile"
on public.profiles
for select
to authenticated
using ((select auth.uid()) = user_id);
create policy "users can update their profile"
on public.profiles
for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);The exact privileges and predicates depend on your data model. A workspace membership table, for example, needs workspace-aware policies rather than a generic auth.uid() = user_id rule copied across every table.
How to inventory RLS and grants together
Run a catalog query in a safe environment and review the result; do not treat an RLS-disabled row as proof of public exposure by itself.
select
n.nspname as schema_name,
c.relname as table_name,
c.relrowsecurity as rls_enabled,
has_schema_privilege('anon', n.nspname, 'usage') as anon_schema_usage,
has_table_privilege(
'anon', format('%I.%I', n.nspname, c.relname), 'select'
) as anon_select,
has_table_privilege(
'authenticated', format('%I.%I', n.nspname, c.relname), 'select'
) as authenticated_select
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
and c.relkind in ('r', 'p')
order by c.relname;Then inspect the policies, including their target roles and commands:
select
schemaname,
tablename,
policyname,
roles,
cmd,
qual as using_expression,
with_check
from pg_policies
where schemaname = 'public'
order by tablename, policyname;Also confirm which schemas the Data API exposes in the Dashboard. A catalog query can show privileges and RLS state, but it does not prove that every route, function, view, or custom-schema setting is safe.
Five RLS mistakes worth reviewing
1. Treating RLS-disabled as proof of anonymous exposure
With RLS off, policies are ignored and standard SQL privileges govern access. That can be dangerous when anon or authenticated has a matching grant through an exposed API path, but no grant means the Data API request is denied. Record all three facts: API exposure, grants, and RLS state.
2. Treating USING (true) as automatically unsafe
USING (true) allows all rows for the policy's target role and command. It may be intentional for public catalog data or an authenticated-only table, and it may be unsafe for customer records. Read the TO role, command, other policies, table grants, and data classification before deciding.
Remember that permissive policies are combined with OR by default. A broad policy can widen a stricter policy that applies to the same role and command.
3. Assuming a missing operation policy creates a leak
For roles subject to RLS, no applicable policy is default deny. Missing INSERT, UPDATE, or DELETE policies usually block that operation; they do not grant it. The risk is broken functionality or a later overly broad fix—not automatic access.
Define each intended operation deliberately, and test denial as well as success.
4. Misreading USING and WITH CHECK
USING controls which existing rows an operation may see or target. WITH CHECK controls whether a newly inserted or updated row is allowed.
For UPDATE or ALL policies, if WITH CHECK is omitted, Postgres reuses the USING expression. Omission is therefore not automatically an ownership-transfer bug. Write an explicit WITH CHECK when the allowed new row differs from the allowed existing row—or when clarity is worth the repetition.
Supabase also notes that an update needs a corresponding select policy so the row can be selected before it is updated.
5. Testing only as the table owner or one happy-path user
Test the roles and identities your app actually uses:
- signed out with a publishable key (
anon); - user A reading and changing user A's allowed rows;
- user A attempting to read or change user B's rows;
- user B performing the reciprocal checks;
- backend code with its intended server-only credentials;
- direct database code separately from Data API code.
Do not use real customer records for an ad hoc test. Seed disposable fixtures in a branch or test project, run allowed and denied cases, and roll the fixtures back.
A safer migration and handoff checklist
Before an AI-generated app goes to a client, require every table migration to answer:
- Is this schema exposed through the Data API?
- Which of
anon,authenticated, andservice_roleneed which table privileges? - Is RLS enabled before client traffic can reach the table?
- Which roles and commands does each policy target?
- Are both allowed and denied cases covered by repeatable tests?
- Does the migration include sequence privileges when an identity or sequence-backed insert needs them?
- Is every secret key confined to server-controlled code?
- Can a new team reproduce the authorization intent without guessing?
For a broader pre-launch review, use the 12-point Supabase RLS checklist. It covers policy inventory, grants, views, functions, storage, and handoff evidence without treating a single catalog flag as a security verdict.