Supabase: How to Fix Explicit GRANTs for a Public Table
A new public table stopped working through the Supabase Data API? Here is the clean way to restore the right GRANTs without overexposing your database.
Since 2026, creating a table in public on Supabase is no longer enough to make it accessible via supabase-js, PostgREST or GraphQL. If your table exists but the API responds with a permission error, the problem is often a missing GRANT, not the RLS.
This is a significant change. Supabase introduced this new logic on April 28, 2026, it becomes the default behavior for new projects on May 30, 2026, then it will be applied to existing projects on October 30, 2026. If you use the Data API, you must therefore integrate the explicit GRANT into your migrations.
The most common symptom
You create a table, you read or write with supabase-js, and you get a response like:
42501: permission denied for table your_table
In many cases, PostgREST even gives you the useful clue in the response: the exact GRANT to add.
The important point to understand is simple:
- the table does exist in Postgres
- the Data API sees it as a protected object
- no API role yet has the right to access it
In other words, your schema is not broken. It's just no longer exposed by default.
Who is really concerned
You are concerned if your application uses one of these routes:
supabase-js- a Supabase client on the browser side
- a direct call to
/rest/v1/ - a direct call to
/graphql/v1/
You are much less concerned if your application only talks to Postgres with a direct connection, for example via an ORM or an application server that does not use the Data API.
This is why many teams are surprised: the database works, the migration goes through, but the auto-generated API does not see the table as accessible until the privileges have been set.
The right reflex: put the GRANTs in the creation migration
The right fix is not to click all over the dashboard. The correct fix is to treat the subject in the normal flow of the schema: SQL migration.
Here is a clean example for a table public.messages exposed to an application with connected users:
grant usage on schema public to anon, authenticated, service_role;
grant select on table public.messages to anon; grant select, insert, update on table public.messages to authenticated; grant select, insert, update, delete on table public.messages to service_role;
alter table public.messages enable row level security;
create policy "users can read their own messages" on public.messages for select to authenticated using (auth.uid() = user_id);
create policy "users can create their own messages" on public.messages for insert to authenticated with check (auth.uid() = user_id); ```
The logic is deliberately separated:
GRANTdecides which roles can reach the table- the RLS decides which lines these roles can read or modify
If you forget the GRANT, the RLS policy is useless, because the role doesn't even make it to the table.
What not to do
The worst reflex is to open too wide just to make the error disappear.
For example, giving ALL PRIVILEGES to anon on all tables public amounts to canceling the hardening that Supabase is precisely imposing. It's fast, but it's not clean, and it's a very bad signal for the rest of the project.
Another common error: believing that RLS replaces GRANT. This is not the case. The two layers work together, not one in place of the other.
A simple rule per role type
In most classic web projects, you can start from this base:
service_role: full access, only for trusted server codeauthenticated: access limited to operations useful to the app, then restricted by the RLSanon: only if public content must be read without connection
This rule already prevents a lot of damage.
If your table contains internal data, keep anon out. If your site displays public content, only give select to anon, no more.
How to verify that the fix is good
Before considering the subject finished, check these 5 points:
- The table has the explicit
GRANTfor the useful roles. - The table has RLS enabled if exposed via the Data API.
- The policies cover the expected reading and writing cases.
- The client code never uses the key
service_role. - Actual reading or writing works with the correct role, not just your admin account.
If you want to take the logic to the end, also add this team rule: no new table public enters the project without GRANT explicit in the migration.
The case of existing projects
If your project already exists, old tables can continue to work using historical privileges. This is practical in the short term, but misleading. This makes everything look clean when in reality the new tables will behave differently.
The healthiest thing is to do a cleaning pass:
- list the tables exhibited
- check which roles have access to what
- provide explicit
GRANTtable by table - no longer rely on automatic privileges for the future
What to remember
If a new table public no longer responds via Supabase, the problem is not always in your code. Since 2026, we must first assume that an explicit GRANT is missing.
The correct method is simple: create the table, place the GRANT, activate the RLS, then add the policies in the same migration. It's clearer, more stable, and much better for project security.
Official sources
Keep reading on the same topic
These links reinforce the blog's topical cluster and help search engines understand the article's primary subject.
