Supahound is in early access. Sign up now; features marked Coming soon switch on as they ship. See what's included

Supahound

Postgres best practices

How to make Supabase RLS policies fast

Slow RLS usually comes from a policy function that Postgres runs once per row, or from a policy column with no index. Wrap `auth.uid()` and `auth.jwt()` in `(select ...)` so Postgres runs them once per query, index every column a policy filters on, and name the role with `to authenticated`.

Last updated

Why wrap auth.uid() in a select?

`auth.uid()` is a function. In `using (auth.uid() = user_id)` Postgres may call it for every row. In `using ((select auth.uid()) = user_id)` the planner turns it into an init plan, runs it once and reuses the value. On large tables this is often the biggest single speed-up.

-- Slow on big tables
using (auth.uid() = user_id)

-- Fast
using ((select auth.uid()) = user_id)

Which columns need an index?

Every column a policy compares, such as `user_id`, `team_id` or `tenant_id`. Without the index, each query scans the whole table before RLS throws most rows away.

create index notes_user_id_idx on public.notes (user_id);

Why name the role in a policy?

A policy without `to authenticated` also runs for the anon role. Naming the role lets Postgres skip the policy entirely for requests it does not apply to.

How do I check team membership without a slow join?

A policy that joins to a membership table runs that join for each row. Flip it: select the user's team IDs once, then test the row's team against that set. Put the lookup in a `security definer` function in a private schema if the membership table has its own RLS.

create policy "docs: team members read" on public.docs
  for select to authenticated
  using (team_id in (
    select team_id from public.memberships
    where user_id = (select auth.uid())
  ));

Should the query filter too?

Yes. Add the same filter in the client query, for example `.eq('user_id', userId)`. RLS stays the security boundary, and the explicit filter gives the planner a better plan.

Frequently asked questions

How do I see if RLS is my bottleneck?
Run `explain analyze` on the query as the authenticated role inside a transaction. Look for sequential scans on large tables and for a policy function called once per row.
Does wrapping auth.uid() change who can see what?
No. The result is the same; Postgres just evaluates the function once per statement instead of once per row.
Are security definer functions safe in policies?
They are safe if you keep them in a schema the API does not expose, set `search_path = ''`, and revoke execute from anon. They run with the owner's rights, so keep them small.

Find out if someone is abusing your app

Connect your Supabase org read-only. The free posture scan runs in minutes. No credit card.