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.
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
profileschecks whether the current user is an admin by selecting fromprofiles. - Two tables’ policies query each other, such as
projectscheckingmemberswhilememberschecksprojects. - 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
- 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.
- Move that lookup into a
security definerfunction in a private schema, withset search_path = ''and fully qualified table names. - 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. - For roles, consider storing them in
app_metadataand checkingauth.jwt() -> 'app_metadata' ->> 'role'. Keep roles out ofuser_metadata, which users can edit themselves. - 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()));Related errors
All errorsNew 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
The database role making the request has no grant on that table, so Postgres refuses before row-level security even runs.
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 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.