← Blog
Technical6 min read2 June 2026

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.

PN
Paul Noorman
Founder, OurPlan
RLS vs GRANT in Supabase: The Difference That Breaks Your API

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 GRANT is 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.

  1. GRANT determines which Postgres roles can reach an object.
  2. 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:

  1. Does the role have a GRANT on the object?
  2. Does the table have RLS enabled?
  3. Does a policy adequately cover the requested action?
  4. 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:

  1. creating the table
  2. GRANT explicit
  3. EPIRB activation
  4. 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

RLS vs GRANT in Supabase: The Difference That Breaks Your API | OurPlan