Supabase Grant: GRANT SELECT and USAGE for Each Role
Give authenticated only what the feature needs, give anon select only on data you'd publish anyway, keep service_role on the server, and keep RLS on.

On this page
- What are the anon, authenticated and service_role roles?
- Which grants should each role get?
- How do you write the grants in a migration?
- When do you need GRANT USAGE as well as GRANT SELECT?
- Does service_role need a grant too?
- Is granting a table to anon a security risk?
- Why not just grant everything like before?
- How do grants and RLS work together?
- Where should you go next?
- FAQ
Short answer: For a Supabase grant to anon and authenticated on a new table, give authenticated only the privileges the feature needs, give anon select only on data you'd publish anyway, and keep service_role for server code. Since Supabase's change, which reaches all existing projects on Oct 30, 2026, new public tables get no grants by default, so you write these lines yourself. Every grant to anon or authenticated should come with row level security on and real policies.
By Gary, Greenlight 路 Last checked against the Supabase changelog and docs: Oct 20, 2026 路 Drafted with AI tools and reviewed before publishing.
Background on the change itself is in our main guide: Will Supabase's October 30 change break your app?. If you're staring at a 42501 error right now, start with Supabase permission denied for table.
Table names in this guide (like notes and posts) are made-up examples. Swap in your own.
What are the anon, authenticated and service_role roles?
Supabase's Data API runs each request as a Postgres role:
- anon: requests made with the public (anon or publishable) key and no signed-in user. Anyone who visits your site can act as this role.
- authenticated: requests from a signed-in user. Any user who can create an account can act as this role.
- service_role: requests made with the secret service role key. Supabase's docs say it bypasses RLS, so it belongs on the server only (Supabase docs (external)).
Before the change, Supabase granted select, insert, update and delete on every new public table to all three roles. After it, those grants are an explicit step (Supabase changelog (external)). Supabase's own reasoning: anon and authenticated need different privileges in most schemas, and explicit grants make that difference visible in your migrations.
Which grants should each role get?
A starting point for common table types. Adjust to your app.
| Table type | anon | authenticated | service_role |
|---|---|---|---|
| Public content (posts, product list) | select | select | as needed by server jobs |
| Private user data (notes, profiles, orders) | none | select, insert, update, delete + owner policies | as needed |
| Write-only form (contact, waitlist) | insert only, if you accept anonymous submissions | insert | select to read submissions |
| Admin or internal (logs, billing records) | none | none, or select behind an admin policy | as needed |
Supabase's RLS docs suggest a similar pattern: revoke the client roles' grants, then grant each role only the operations it needs (Supabase docs (external)). Rule of thumb: no grant at all is the safest default for a table the browser never touches. The table then stays off the Data API for that role, and your server can still use it.
How do you write the grants in a migration?
Put the grant right after the create table, in the same migration, so nobody forgets it:
create table public.notes (
id bigint generated always as identity primary key,
user_id uuid not null default auth.uid() references auth.users (id),
body text not null,
created_at timestamptz not null default now()
);
alter table public.notes enable row level security;
-- Signed-in users only; no anon access
grant select, insert, update, delete on table public.notes to authenticated;
grant select, insert, update, delete on table public.notes to service_role;
create policy "own notes" on public.notes
for all to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );And a public, read-only table:
alter table public.posts enable row level security;
grant select on table public.posts to anon, authenticated;
create policy "published posts are public" on public.posts
for select to anon, authenticated
using ( published = true );Grant syntax and privilege names follow the PostgreSQL GRANT command (PostgreSQL docs (external)). You can confirm the result with the information_schema.role_table_grants query from Supabase's 42501 troubleshooting page (Supabase docs (external)).
When do you need GRANT USAGE as well as GRANT SELECT?
grant select (and insert, update, delete) works on a table. grant usage works on a schema: Postgres won't let a role reach any table in a schema it has no USAGE on (PostgreSQL docs (external)). Supabase projects normally already give the client roles usage on public, so for public tables the table grants above are usually all you add. If you get "permission denied for schema", or you put tables in your own schema, add the schema grant first:
grant usage on schema public to anon, authenticated;Does service_role need a grant too?
Yes, if your server reaches the table through the Data API with the secret key. The changelog lists service_role alongside anon and authenticated as roles that used to get the default grants (Supabase changelog (external)). Bypassing RLS doesn't skip the grant check: RLS and grants are separate layers. If your server uses a direct Postgres connection instead, the change doesn't affect it.
Keep the service role key out of browser code, mobile apps and public repos. If it has leaked, follow our service role key guide.
Is granting a table to anon a security risk?
It can be. A grant to anon means anyone holding your public key, which is every visitor, can send that kind of request. What protects the data is RLS:
| anon grant | RLS | What a stranger with your public key can do |
|---|---|---|
| none | any | nothing on this table |
select | on, policy published = true | read published rows only |
select | on, no policies | read nothing (default deny) |
select | off | read every row |
The last row is the classic leak. The public key itself is fine to ship; see is my Supabase anon key safe?. The risk is a grant plus missing RLS.
Why not just grant everything like before?
You can, but it undoes the point of the change. Supabase says the old default exposed every public table on creation, "including tables a developer forgot to protect" (Supabase changelog (external)). Granting per table and per role keeps a forgotten table off the API. Also avoid grant all: PostgreSQL says it grants "all of the privileges available for the object's type," which for a table includes truncate and trigger, privileges browser roles should never need (PostgreSQL docs (external)).
How do grants and RLS work together?
Grants decide if a role can touch the table at all; RLS decides which rows. Supabase states that RLS behavior is unchanged by this switch (Supabase changelog (external)). The full comparison, with a debugging order, is in Supabase GRANT vs RLS.
Where should you go next?
If you're seeing the error right now, start with Supabase permission denied for table. For how grants and policies combine, read Supabase GRANT vs RLS. If a secret key ever reached your frontend, see Supabase service role key exposed?. Building with AI tools? Our Lovable launch checklist and what is vibe coding? cover the rest of a pre-launch pass, and the hub for this change is Will Supabase's October 30 change break your app?
FAQ
Should I grant to anon if my app requires login?
Usually not. If every page needs a signed-in user, grant to authenticated only.
Do I need to redo grants on existing tables?
No. Supabase says existing tables keep their current grants. Do review them, though: old defaults may have given anon more than it needs.
Can I grant just some columns?
Postgres supports column-level grants, for example grant select (id, title) on public.posts to anon; (PostgreSQL docs (external)). Test them with your client library before relying on them.
What about my local stack? Local rebuilds hit the same issue. See Supabase db reset permission denied.
Sources
On this site
Related posts

Lovable Supabase Permission Denied? Fix for Bolt Apps Too
A Lovable or Bolt app saying Supabase permission denied usually means a new table has no GRANT. A safe prompt for your AI tool and how to check it.

Postgres Row Level Security: A Supabase Guide
Postgres row level security decides which rows each user can read or change. Set it up in Supabase, write policies and fix common errors.

Supabase db reset: Fix Permission Denied After Oct 30
Supabase db reset permission denied (42501) happens because rebuilt tables no longer get default grants. Add GRANTs to migrations and retest.
Get the next change before it bites
Greenlight tips and new blog posts: short, practical notes when Supabase, Lovable, Bolt, Cursor or another tool you build with changes something that affects your app, plus what to check. About twice a month.
We'll send a confirmation email first. Unsubscribe anytime. Privacy. Greenlight My App is independent and not affiliated with or endorsed by Supabase, Lovable, Bolt, Cursor or any other tool we mention.
Greenlight is independent and not affiliated with or endorsed by Supabase or PostgreSQL. Product names are used only to identify the tools this guide is about. Code is example SQL; test it on a copy of your project before running it in production. This page is general information, not professional security advice, legal advice, a security audit or a guarantee. Check your own setup against the vendor's current docs.