---
title: "Supabase 42501 permission denied: a grant, not RLS"
description: "Why Supabase returns 42501 permission denied for a table, what changes on 30 October 2026, and the grant, RLS and policy that fix it, reproduced locally."
url: "https://madsalt.tech/notes/supabase-42501-permission-denied/"
author: "Denys Zaichenko"
language: "en"
published: 2026-10-26
---

# Supabase 42501 permission denied: a grant, not RLS

TL;DR

`42501 permission denied for table …` means the role has no grant on the table, so Postgres stops the request before any RLS policy runs. From 30 October 2026 Supabase stops granting new tables in `public` automatically on existing projects, so tables created after that answer 42501 until you grant them. The fix is three statements together: a grant for the operations the role needs, `enable row level security`, and a policy. Granting alone, without RLS, opens the table to everyone.

I built a free [Supabase security checker](https://madsalt.tech/tools/supabase-security-check/) that reads grants on every exposed table, and 42501 is the error people hit when grants are wrong in the other direction: too few instead of too many. Here is what it means and what I saw when I reproduced it.

## What does error 42501 mean in Supabase?

42501 is the Postgres code for insufficient privilege. Postgres checks two layers before a client touches a table: grants decide whether the role may run the operation at all, and RLS policies decide which rows. A missing grant fails first, as an error. A policy that matches nothing fails quietly, as an empty list. So when you see 42501, look at grants, not at your policies.

PostgREST, the Data API behind `supabase-js`, passes the error through with a hint. On my local stack (Supabase CLI 2.109.1, Postgres 17.6, PostgREST 14.14), a request with the publishable key to a table without a grant for `anon` returned HTTP 401 and:

```json
{
  "code": "42501",
  "details": null,
  "hint": "Grant the required privileges to the current role with: GRANT SELECT ON public.orders TO anon;",
  "message": "permission denied for table orders"
}
```

## Why do new tables get 42501 after 30 October 2026?

Until now, a new table in `public` got `select`, `insert`, `update` and `delete` for `anon`, `authenticated` and `service_role` through default privileges, so it was reachable over the Data API the moment it was created, protected or not. Supabase's [changelog](https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically) removes those defaults: optional from 28 April 2026, default for new projects from 30 May 2026, and applied to all existing projects on 30 October 2026. Existing tables keep their grants.

What else the switch breaks, and four checks to run before it: [Supabase October 30 grant change: what breaks, how to check](https://madsalt.tech/blog/supabase-october-30-grant-change/).

I applied the same change to my local project with the two `alter default privileges … revoke` statements from the changelog, then created a new table `invoices`:

![What a new table answers after 30 October 2026. Local Supabase with the new default privileges, one request each. Publishable key, no grant: create table → no grant → 42501, HTTP 401; Secret key, no grant: create table → no grant → 42501, HTTP 403; After grant select and enable RLS: grant select → RLS on → 0 rows, HTTP 200.](https://madsalt.tech/charts/en/supabase-42501-permission-denied.svg)

*My reproduction on a local Supabase stack with the default privileges from the Supabase changelog, 23 October 2026.*

| Request to the new table                                              | Result                                                           |
| --------------------------------------------------------------------- | ---------------------------------------------------------------- |
| Publishable key, no grant                                             | HTTP 401, 42501, hint `GRANT SELECT ON public.invoices TO anon;` |
| Secret key, no grant                                                  | HTTP 403, 42501, hint `… TO service_role;`                       |
| Publishable key, after `grant select` and `enable row level security` | HTTP 200, `[]` until a policy allows rows                        |

Two things surprised me. The secret key is refused too: `service_role` bypasses RLS, but it still needs a grant, so an Edge Function or a server job that writes to a new table breaks the same way. And the revoke takes back only those four operations: the new table still came with `REFERENCES`, `TRIGGER` and `TRUNCATE` for `anon`, `authenticated` and `service_role`. No policy applies to `TRUNCATE`.

## How do you fix 42501 without opening the table?

Treat the grant, RLS and the policy as one change, in one migration, and grant each role only what it needs:

```sql
revoke all on table public.invoices from anon, authenticated;
grant select, insert, update on table public.invoices to authenticated;
grant select, insert, update, delete on table public.invoices to service_role;

alter table public.invoices enable row level security;

create policy "Owners read their invoices"
on public.invoices for select
to authenticated
using ((select auth.uid()) = user_id);
```

The `revoke all` first clears the `TRUNCATE`, `TRIGGER` and `REFERENCES` that came with the table. Do not answer 42501 with `grant all on all tables in schema public to anon`: that is the old default back, for every table at once, and any table without RLS becomes readable by anyone with your public key.

If an AI tool creates your tables, tell it to emit grants, RLS and policies in the same migration. The hint in the error is written so an agent can fix the grant itself, but the hint names only the grant, not the RLS that should come with it.

## How does the checker see this?

Four of its rules read the grants layer: "Default privileges that open new objects to the API" flags projects that still grant new tables automatically, "Unused write grants for anonymous visitors" finds `anon` write grants no policy uses, and "Unneeded table privileges for signed-in users" finds `TRUNCATE`, `TRIGGER` and `REFERENCES` like the ones above. "Policies that never run: the role has no grant" lists tables where a policy names a role that has no grant for that command, which is how a new table looks after 30 October. Why RLS still matters after the grant is in [how to test Supabase RLS](https://madsalt.tech/blog/how-to-test-supabase-rls/), and what grants and policies look like across a whole app is in my [Lovable app security checklist](https://madsalt.tech/blog/lovable-app-security-checklist/).
