Supabase row-level security, explained for builders
If you only fix one thing before launch, make it this: which keys are safe in the browser, and how to make sure user A can never read user B's data.
From October 30, 2026, new public tables in existing Supabase projects aren't reachable through the Data API until you grant access. What changes, what the error looks like, and the safe fix.

If your app talks to Supabase through supabase-js or another client library, one default changes on October 30, 2026. We'll cover what changes, what the error looks like, and how to fix it without weakening your security.
Until now, new tables in the public schema were automatically granted select, insert, update and delete for the anon, authenticated and service_role roles. That made every new table reachable through the Data API as soon as it was created (Supabase changelog).
After the change, Supabase says new tables in public "require an explicit opt-in (via a Postgres grant) before the Data API can see them" (Supabase changelog). The Data API is the auto-generated REST and GraphQL layer that supabase-js and the other client libraries call (Supabase changelog).
The dates, from the changelog (Supabase changelog):
What's not affected, per the changelog (Supabase changelog):
storage, auth, realtime and custom schemas keep their current grants and defaults.The changelog explains why. Today AI tools, agents and scripts create tables too, often with no human reviewing the change, so it's easy to expose a table before it's protected (Supabase changelog).
Grant, enable RLS, add policies: treat them as one unit, in the same migration.
If a grant is missing, the Data API returns a Postgres 42501 error instead of failing silently. Supabase's example (Supabase changelog):
{
"code": "42501",
"message": "permission denied for table your_table",
"hint": "Grant the required privileges to the current role with: GRANT SELECT ON public.your_table TO anon;"
}
Supabase's troubleshooting page notes that clients often report 42501 errors as 401 or 403 errors. It also lists other causes, such as restricted schemas, column-level restrictions and RLS blocking an insert or update (Supabase: Database API 42501 errors). So read the message. "permission denied for table" points to a missing grant. "new row violates row-level security policy" points to a policy (see our Supabase RLS guide).
To see which grants a table has, Supabase suggests (Supabase: Database API 42501 errors):
select grantee, privilege_type
from information_schema.role_table_grants
where table_name = 'your_table';
Supabase recommends treating three steps "as a unit" in the same migration: grant the privileges each role needs, enable RLS, and add policies (Supabase changelog). Without the grant, "Postgres rejects the query before RLS comes into play" (Supabase changelog).
Here's what that looks like for a table where signed-in users manage only their own rows:
create table public.todos (
id bigint generated by default as identity primary key,
user_id uuid not null default auth.uid(),
task text
);
-- 1. Grant only what each role needs (here: nothing for signed-out visitors)
grant select, insert, update, delete on table public.todos to authenticated;
-- 2. Keep RLS on
alter table public.todos enable row level security;
-- 3. One policy per operation
create policy "todos select own" on public.todos
for select to authenticated using ( (select auth.uid()) = user_id );
create policy "todos insert own" on public.todos
for insert to authenticated with check ( (select auth.uid()) = user_id );
create policy "todos update own" on public.todos
for update to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );
create policy "todos delete own" on public.todos
for delete to authenticated using ( (select auth.uid()) = user_id );
The grant syntax follows the changelog's example (Supabase changelog). We've made a few deliberate changes:
anon grant here,** because signed-out visitors shouldn't reach this table. The changelog's example does grant select to anon. Only do that for data meant to be public.service_role grant here.** The changelog's example also grants service_role. Add grant select, insert, update, delete on table public.todos to service_role; only if server-side code uses the secret key for this table.(select auth.uid()),** following the RLS guide (Supabase: RLS).When an app suddenly breaks, a quick "fix" can make things worse. Avoid these:
service_role key.** It bypasses RLS and must never be in a browser or a shipped app (Supabase: API keys).anon without RLS and policies.** Supabase's troubleshooting page notes that granting privileges allows access through the Data API, so enable RLS and write policies to protect the data (Supabase: Database API 42501 errors).My Supabase app returns 42501 "permission denied for table <name>". Fix it with
explicit GRANT statements in a migration. Grant only the privileges each role
needs (anon only for data that is meant to be public). Keep row level security
enabled, and add one policy per operation (select, insert, update, delete) using
(select auth.uid()). Do not disable RLS, do not use using (true) on private data,
and do not use the service_role or secret key in frontend code. Show me the SQL
before applying it.
Always read what the tool produces before you run it. Then test with two accounts, as our Supabase RLS guide describes.
public tables get explicit grants in the same migration as RLS and policies (Supabase changelog)anon only has grants on data meant to be publicHas an AI tool ever "fixed" a permission error by turning off a safety setting? Tell us what happened and how you caught it. We'll collect the patterns for a future guide.
This guide is educational and based on the vendor's public docs as of the review date. Platforms change: check the linked sources. Not a security audit. Disclaimer.
The BuiltSafely weekly
Platform changes that affect the security of AI-built apps, checked against official docs. Free. Unsubscribe any time.