Supabase RLS error
infinite recursion detected in policy for relation
"infinite recursion detected in policy for relation" (Postgres error 42P17) means a row level security policy on a table reads that same table, directly or through another table's policy that reads back: to check the policy, Postgres has to apply the policy again. The classic case in Supabase apps is an admin or role check such as using ((select role from profiles where id = auth.uid()) = 'admin') on profiles itself. Every query on the table then fails, and the REST API returns HTTP 500 with code 42P17. Move the lookup into a security definer function in a schema the Data API does not expose, with set search_path = '', and call it from the policy as (select private.is_admin()). Reproduced on a local Supabase stack (Postgres 17.11, PostgREST 16.5) and checked against the Postgres source on 2026-10-27.

The helper function is callable by signed-in users once you grant it. For what that exposes, see whether anyone can call your Supabase RPC functions with the anon key.
Why the policy loops
Postgres applies a table's policies to every query on it, including the sub-select inside a policy. A policy on profiles that selects from profiles makes Postgres apply the profiles policies to that sub-select, which contains the same sub-select, and so on; Postgres stops while it rewrites the query and raises 42P17 before it reads a row. The same happens across two tables: a policy on teams that reads members, and a policy on members that reads teams.
-- loops: the policy on profiles reads profiles
create policy "admins can read all profiles" on public.profiles
for select to authenticated
using ((select role from public.profiles where id = (select auth.uid())) = 'admin');The fix: a security definer function outside the exposed schemas
A security definer function runs as its owner. Owned by postgres, its query skips RLS, so the policy no longer calls itself. Supabase's RLS guide uses the same pattern, a function in a private schema with set search_path = '', and says never to create one in a schema listed under Exposed schemas, because the Data API would let users call it directly. Grant execute to authenticated only: in our run, without that grant the request failed with permission denied for function is_admin.
create schema if not exists private;
create or replace function private.is_admin()
returns boolean
language sql
security definer
set search_path = ''
stable
as $$
select exists (
select 1 from public.profiles
where id = (select auth.uid()) and role = 'admin'
);
$$;
revoke execute on function private.is_admin() from public, anon;
grant execute on function private.is_admin() to authenticated;
drop policy "admins can read all profiles" on public.profiles;
create policy "admins can read all profiles" on public.profiles
for select to authenticated
using ((select private.is_admin()));Keep roles out of the table the policy protects
A separate user_roles table read only through the definer function, or a custom claim in the JWT read with auth.jwt(), avoids the loop for good. Do not read a role from user_metadata in a policy: users can change their own user_metadata. Supabase's RLS guide says the same and points to app_metadata, which only the server sets.
Lovable and other generated apps
Generated admin panels often add a policy like "Admins can view all profiles" that checks the role on profiles itself, and the app breaks the moment it is applied: every page that reads profiles shows an error, or an empty list where the client swallows it. Ask the agent for a security definer has_role or is_admin function in a private schema, and check the migration it writes against the code above.
Checklist
How to fix it, then check the fix
- 01Read the relation name in the error: it is the table whose policies loop, not always the table you queried.
- 02List that table's policies and look for a sub-select of the same table, or of a table whose policy reads back:
select policyname, cmd, qual, with_check from pg_policies where schemaname = 'public' and tablename = 'profiles'; - 03Move the lookup into a security definer function in a schema that is not exposed, with set search_path = '', owned by postgres.
- 04Revoke execute on the function from public and anon, and grant it to the roles the policy is for.
- 05Run the same request as a normal user and as an admin: each gets 200 and only the rows it should see.
Related
Related guides
Sources
Official docs behind this checklist
Browse all launch guides · Read resources · How to make and check your next change
FAQ
FAQ for builders before launch
Does disabling RLS fix it?
It stops the error and exposes every row to anyone with the anon key. Fix the policy instead.
Why does it fail even for signed-out requests?
Postgres detects the loop while it rewrites the query, before it reads any rows or checks who you are, so every role the policy applies to gets the error.
Is a security definer function safe here?
It is the pattern Supabase documents, as long as it lives in a schema the API does not expose, sets search_path to an empty string, returns only a boolean, and only the roles that need it can execute it.