Guides / infinite recursion detected in policy

last checked nov 2026

Supabase "infinite recursion detected in policy for relation": what it means and how to fix it

Your query fails with infinite recursion detected in policy for relation "profiles" (Postgres error 42P17). A Row Level Security policy reads a table whose policies lead back to the original policy, so Postgres stops the query. The fix is to break the loop. Turning RLS off is not a fix.

The short answer

Supabase's troubleshooting article describes it as a circular dependency between RLS policies. It happens when:

The error can also show up on Storage requests, if a Storage policy references a table with self-referencing or mutually recursive policies. Supabase gives two fixes: rewrite the policies so there's no cycle, or move the lookup into a security definer function.

How to find the loop

1. List the policies that read other tables

Run this in the SQL Editor. It prints policy expressions only, never data or keys:

select tablename, policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'public'
  and (qual ilike '%select%' or with_check ilike '%select%')
order by tablename;

Look for a policy whose qual or with_check selects from its own tablename, or two tables whose policies select from each other.

2. Reproduce it as a real user

Table owners and superusers normally skip RLS, so a test from the SQL Editor's default connection may not show the error. Switch to the app's role inside a transaction and roll back:

begin;
set local role authenticated;
set local request.jwt.claim.sub = '00000000-0000-0000-0000-000000000001'; -- a real user's id
select count(*) from public.profiles;
rollback;

How to fix it

Option 1: remove the cycle

Often one side only needs to check the caller's own rows. A membership policy such as using ( (select auth.uid()) = user_id ) doesn't read any other table, so the other table's policy can query it without looping back.

Option 2: a security definer helper in a private schema

Supabase's RLS guide says a security definer function runs as the role that created it, and when that's a superuser like postgres, the function has bypassrls. The lookup inside it doesn't trigger the table's policies, so the loop never starts:

That holds while the owner can bypass RLS: a function owned by a role without bypassrls, or one reading a table set to force row level security, evaluates the policy again and stays recursive.

create schema if not exists private;

create function private.is_admin()
returns boolean
language plpgsql
security definer        -- runs as the creator
set search_path = ''    -- every name inside must be schema-qualified
stable
as $$
begin
  return exists (
    select 1 from public.profiles
    where id = (select auth.uid()) and role = 'admin'
  );
end;
$$;

revoke execute on function private.is_admin() from public, anon;
grant usage on schema private to authenticated;
grant execute on function private.is_admin() to authenticated;

drop policy if exists "Admins read all profiles" on public.profiles;
create policy "Admins read all profiles"
on public.profiles for select
to authenticated
using ( (select private.is_admin()) );

Three details matter:

Wrapping the call in (select ...) lets Postgres run it once per statement instead of once per row, per Supabase's RLS guide.

Don't "fix" it by turning protection off. alter table ... disable row level security on a membership or profiles table stops the error and can make that table readable by any role with a grant on it, which on many projects includes anon. A policy of using (true) or a service_role key in your frontend have the same effect. See RLS policies that are always true and service_role key exposed in client code.

Confirm it worked

Re-run the step 2 test as an admin user and as a normal user: both should return without the error, and the normal user should only see the rows the policies allow. Then run it with set local role anon; and confirm signed-out requests see nothing they shouldn't.

free check

Did a secret key end up in your frontend?

If you pasted a service_role or secret key into client code while chasing this error, LockedVibe's free Leak Checker can look at your live site for exposed keys, the way any visitor's browser would. It can't see your database policies. For a full review, see the $99 Scan + Report.

Sources

Every Postgres and Supabase detail above was checked against these official pages. Last checked Nov 2026. If behavior changed, the docs below are the source of truth.