Infinite recursion detected in policy for relation

A row-level security policy reads from its own table, so checking the policy runs the same policy again, without end.

Supabase and browser console
infinite recursion detected in policy for relation "profiles"

Prompt to fix it

Queries fail with "infinite recursion detected in policy for relation". Find the row-level security policy that reads from its own table, or two policies that read each other’s tables. Move that lookup, such as the admin or membership check, into a security definer function in a private schema with search_path set to empty and fully qualified names, then call it from the policy as (select private.is_admin()). Keep RLS enabled on every table, don’t store roles in user_metadata, and show me the SQL before you run it.

Paste into Layerwise or any AI coding tool.

What it means

To check a row-level security (RLS) policy, Postgres runs the policy’s condition. If that condition reads from the same table, Postgres has to apply the table’s policies to that read as well, which runs the condition again. Postgres spots the loop and stops the query with this error, and Supabase’s API answers with status 500.

The classic case is an admin check. A policy on profiles says “admins can read every profile” and finds admins with exists (select 1 from profiles where id = auth.uid() and role = 'admin'). That inner select on profiles is the loop. Two tables whose policies query each other loop the same way.

The fix is to do the lookup outside RLS. A security definer function runs with its owner’s rights, so the check inside it skips the table’s policies. Keep that function in a schema the API doesn’t expose, so nobody can call it directly. Another option is to store the role in the user’s app_metadata and read it with auth.jwt(), with no table lookup at all.

Common causes

  • A policy on profiles checks whether the current user is an admin by selecting from profiles.
  • Two tables’ policies query each other, such as projects checking members while members checks projects.
  • A “members can see other members” policy looks up membership in the same table it protects.
  • A view or function used inside the policy reads the same table with RLS still applied.

How to fix it

  1. In the Supabase dashboard, open the table’s policies and find the one whose condition selects from the same table, or from a table whose policy selects back.
  2. Move that lookup into a security definer function in a private schema, with set search_path = '' and fully qualified table names.
  3. Replace the subquery in the policy with a call to that function, written as (select private.is_admin()) so Postgres runs it once per query.
  4. For roles, consider storing them in app_metadata and checking auth.jwt() -> 'app_metadata' ->> 'role'. Keep roles out of user_metadata, which users can edit themselves.
  5. Keep RLS on the table while you fix it.

Example fix

-- Look up the role outside RLS so the policy doesn't query itself.
create schema if not exists private;

create or replace function private.is_admin()
returns boolean
language sql
stable
security definer
set search_path = ''
as $$
  select exists (
    select 1 from public.profiles
    where id = (select auth.uid()) and role = 'admin'
  );
$$;

drop policy if exists "Admins can read all profiles" on profiles;
create policy "Admins can read all profiles" on profiles
for select to authenticated
using ((select private.is_admin()));
All errors
new row violates row-level security policy for table "todos"

New row violates row-level security policy

Postgres refused to save a row because no row-level security policy on that table lets the current user write it.

permission denied for table todos

Permission denied for table

The database role making the request has no grant on that table, so Postgres refuses before row-level security even runs.

POST https://my-app.com/api/checkout 500 (Internal Server Error)

500 Internal Server Error

The server’s code threw an error or crashed while handling the request, and the real error message is waiting in the server logs.

relation "todos" does not exist

Relation does not exist

The query names a table that Postgres can’t find in this database, usually because it was never created here or has a different name.

Build with an agent that checks its work

The Layerwise agent clicks through the live preview and reads the console, so it catches errors before you do.