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.

Gary 路 Published 路 Updated 路 7 min read

Four-step card titled Grants for anon and authenticated: anon select on public data only, authenticated what the feature needs, service_role server only, RLS on every table.
On this page
  1. What are the anon, authenticated and service_role roles?
  2. Which grants should each role get?
  3. How do you write the grants in a migration?
  4. When do you need GRANT USAGE as well as GRANT SELECT?
  5. Does service_role need a grant too?
  6. Is granting a table to anon a security risk?
  7. Why not just grant everything like before?
  8. How do grants and RLS work together?
  9. Where should you go next?
  10. 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 typeanonauthenticatedservice_role
Public content (posts, product list)selectselectas needed by server jobs
Private user data (notes, profiles, orders)noneselect, insert, update, delete + owner policiesas needed
Write-only form (contact, waitlist)insert only, if you accept anonymous submissionsinsertselect to read submissions
Admin or internal (logs, billing records)nonenone, or select behind an admin policyas 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:

SQL

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:

SQL

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:

SQL

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 grantRLSWhat a stranger with your public key can do
noneanynothing on this table
selecton, policy published = trueread published rows only
selecton, no policiesread nothing (default deny)
selectoffread 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

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.