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.