RLS vs GRANT in Supabase: The Difference That Breaks Your API
Many teams confuse RLS and GRANTs in Supabase, even though they solve two different access problems.
When a Supabase request fails, many teams immediately say: "it's the RLS". Sometimes yes. But very often, the real problem is higher up: the role is not even allowed to reach the table, so the RLS does not yet come into play.
The most useful distinction to remember is this:
GRANT: can the role reach the table, view or function via the Data API?- RLS: once the table is reached, which rows can it read, insert, modify or delete?
If you confuse these two layers, you waste time and risk correcting the wrong thing.
The clearest shortcut
Think of a door and a filter.
- the
GRANTis the entrance door - the RLS is the filter inside
Without GRANT, the door remains closed. With a GRANT but without a suitable policy, you may enter the room, but you cannot see or do anything.
What Supabase says
The Supabase documentation is very clear on this point: the Data API is based on two layers that work together.
GRANTdetermines which Postgres roles can reach an object.- RLS policies determine which lines these roles can read or modify.
This is exactly why a table can be:
- accessible but too open
- accessible but empty
- inaccessible despite correct policies
Case number 1: the GRANT is missing
This is the simplest case to diagnose. Your request returns a permission error, often with the code 42501.
Typical example:
- you create a table
public.projects - you activate the EPIRB
- you add your own policy
- but you forget the
grant select on table public.projects to authenticated;
Result: the policy may be perfect, the user does not reach the table.
This is why the Supabase documentation recommends treating GRANT and RLS in the same migration.
Case number 2: the GRANT exists, but the RLS blocks everything
Here, the table is accessible, but reading or writing remains impossible because the policy does not let any useful line pass.
PostgreSQL documents it very clearly: when row security is enabled and no policy matches, default deny logic applies.
In practice, this often gives symptoms like:
- a query that returns no row even though there is plenty of data
- an insertion refused by the policy
- an update that doesn't affect anything
In this case, the GRANT is not in question. It is the RLS that needs to be reviewed.
Case number 3: the GRANT exists, but the table is too open
The opposite also exists. A table can be reachable via the Data API with privileges that are too broad, especially on historical projects that have kept permissive defaults.
If RLS is not enabled, or if the policy is too broad, you expose more data than intended.
This is often more dangerous than a visible error, because everything seems to "work". Yet the real problem is too generous exposure.
A concrete example
Let's imagine a table public.messages with a column user_id.
Product objective:
- an unconnected visitor sees nothing
- a connected user can read their own messages
- the back-end server can manage everything
The minimal schema looks like this:
grant usage on schema public to anon, authenticated, service_role;
grant select, insert 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); ```
In this example:
- without the
GRANT, the user does not reach the table - without the policies, the user reaches the table but should not see anything
- without EPIRB activated, you run the risk of too broad access
Simple table to avoid making mistakes
| Location | GRANT | RLS | Result | | --- | --- | --- | --- | | Table inaccessible | absent or insufficient | correct or not | permission error, often 42501 | | Table accessible but empty | present | too strict or absent depending on the case | zero line or refusal on the action | | Table accessible and too open | too wide | absent or too wide | possible data leak | | Well secured table | adapts | adapted | correct access, limited to product need |
How to quickly diagnose
When a request poses a problem, don't leave it at random. Follow this order:
- Does the role have a
GRANTon the object? - Does the table have RLS enabled?
- Does a policy adequately cover the requested action?
- Does the policy condition really match your data?
This method avoids spending an hour on a policy when the table is not even reachable.
The classic trap with service_role
The service_role role must remain reserved for trusted server code. This is not a patch to resolve a customer problem.
If you put service_role on the browser side, you bypass the project's security model instead of fixing it. This is never the right solution for a public site or customer portal.
A good migration rule
If a table must be exposed via the Data API, always group these 4 blocks in the same migration:
- creating the table
GRANTexplicit- EPIRB activation
- policies
This discipline forces legible security. And above all, it avoids situations where a table "works" locally or in admin, then breaks in the real user journey.
What to remember
The difference between GRANT and RLS is not theoretical. It is often she who decides if your API is:
- inaccessible
- too open
- or finally correct
The GRANT opens the door. The RLS filters the lines. Until this distinction is clear, you risk correcting the wrong problem.
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.
