DEV Community

Cover image for GRANT ALL to anon is the wrong fix for Supabase 42501 after Oct 30
PatchAndShip
PatchAndShip

Posted on Originally published at patchandship.com

GRANT ALL to anon is the wrong fix for Supabase 42501 after Oct 30

From Oct 30, 2026, Supabase will apply a change to existing projects: new tables in public won't be visible to the Data API until you grant access. Supabase's changelog puts it like this:

"Once the change is rolled out to your project, new tables you create in public schema require an explicit opt-in (via a Postgres grant) before the Data API can see them. Existing tables are not affected in your project, they keep their current grants and stay reachable."

Once the change reaches your project, the first new table you add will likely fail with 42501 permission denied for table. And the quickest-looking fix, often the one an AI assistant suggests, is this:

-- Don't run this
grant all on all tables in schema public to anon;
Enter fullscreen mode Exit fullscreen mode

If you want the full walkthrough of what 42501 means, we covered it in the first article. This one is about the fix: keep RLS on, grant only what each table needs, and check your older tables too.

Read the error before you fix it

Three results look alike but mean different things:

  • 42501 permission denied for table: the role has no grant. You need a specific grant.
  • An empty list, no error: the grant is there, and RLS is hiding rows. Check your policies.
  • 42501 new row violates row-level security policy: RLS is working and blocked the write. Fix the policy or the data you send, not the grants.

Only the first one is a grant problem. A broad grant does nothing for the other two.

Why GRANT ALL to anon is wrong

anon is the role behind your public anon key, and that key ships in every browser that loads your app. Any signed-out request runs as anon.

GRANT ALL gives that role every table privilege Postgres has: SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER, and, on Postgres 17 and later, MAINTAIN. A signed-out visitor has no use for most of those.

After that, RLS is the only thing deciding what happens. If RLS is off on a table, or a policy is permissive (like using (true)), anyone with your public key can read or write that table. The grant removed one layer of protection and left everything to the other.

Step 1: turn RLS on first

Enable RLS before you grant anything, in the same migration:

alter table public.your_table enable row level security;
Enter fullscreen mode Exit fullscreen mode

With RLS on and no policies, the table returns nothing to API roles. That's a safe starting point. Then add the grants and policies the app actually needs.

Step 2: grant only the verbs each table uses

For a typical per-user table, signed-in users work with their own rows and signed-out visitors need nothing:

alter table public.your_table enable row level security;

grant select, insert, update on public.your_table to authenticated;

create policy "Users read own rows"
  on public.your_table for select
  to authenticated
  using ((select auth.uid()) = user_id);

create policy "Users insert own rows"
  on public.your_table for insert
  to authenticated
  with check ((select auth.uid()) = user_id);

create policy "Users update own rows"
  on public.your_table for update
  to authenticated
  using ((select auth.uid()) = user_id)
  with check ((select auth.uid()) = user_id);
Enter fullscreen mode Exit fullscreen mode

Add delete (and a matching delete policy) only if the app deletes rows. No grant to anon at all: a signed-out request gets a 42501, which is correct here.

For a table that really is public, like a published product list, anon gets select and nothing else, with a policy that limits which rows:

alter table public.products enable row level security;

grant select on public.products to anon;

create policy "Anyone reads published products"
  on public.products for select
  to anon, authenticated
  using (published = true);
Enter fullscreen mode Exit fullscreen mode

If the table has a serial id, the role that inserts also needs the sequence:

select pg_get_serial_sequence('public.your_table', 'id');
-- usually returns public.your_table_id_seq
grant usage on sequence public.your_table_id_seq to authenticated;
Enter fullscreen mode Exit fullscreen mode

Step 3: check your older tables too

The changelog says existing tables "keep their current grants and stay reachable". Tables created under the old defaults may give anon more than a new one would, so review them too.

These two queries only read metadata. First, what anon and authenticated can do on each table:

select table_name, grantee, string_agg(privilege_type, ', ' order by privilege_type) as privileges
from information_schema.role_table_grants
where table_schema = 'public'
  and grantee in ('anon', 'authenticated')
group by table_name, grantee
order by table_name, grantee;
Enter fullscreen mode Exit fullscreen mode

Second, which tables have RLS off:

select tablename
from pg_tables
where schemaname = 'public'
  and rowsecurity = false
order by tablename;
Enter fullscreen mode Exit fullscreen mode

Start with any table that's in the second list and also shows grants for anon or authenticated in the first. Before you revoke anything, check what your signed-out pages actually read, so you don't break a public page. To take back an anon grant and keep only reads:

revoke all on public.your_table from anon;
grant select on public.your_table to anon; -- only if the table is truly public
Enter fullscreen mode Exit fullscreen mode

Quick check

  • Which 42501 is it: "permission denied" or "violates row-level security policy"?
  • RLS enabled on the table before any grant?
  • anon has no grant, or only select on a truly public table?
  • authenticated has only the verbs the app uses, each with an owner policy?
  • serial id? Sequence usage granted to the inserting role?
  • Older tables reviewed with the two queries above?
  • Tested as a real signed-in and signed-out user, not in the SQL editor?

That last point matters. The SQL editor runs as postgres, which bypasses RLS, so a query that works there tells you nothing about what your users see. Test from the app, or use the dashboard's user impersonation to run queries as a specific role or user.

Recap

When Oct 30 reaches your project and a new table throws 42501, the fix is a few lines, not one big grant: RLS on first, the specific verbs each role needs, and a review of the tables you already have.

Not affiliated with Supabase.

Want a quick outside look at what your app's public files expose? Run the free check.

Top comments (0)