Back to Help

"Infinite recursion detected in policy for relation" in a Lovable + Supabase app

Short answer: one of your row-level security (RLS) policies looks up the same table it is protecting. To check that lookup, Postgres has to run the same policy again, which runs the lookup again, and so on, so Postgres stops and throws error 42P17. The fix is to move the "is this person a member / an admin?" check into a small helper function that is allowed to read the table without re-running the policy, then point the policy at that function. You do not need to turn RLS off, and you shouldn't.

You might see:

What you seeWhere
infinite recursion detected in policy for relation "profiles" (or members, team_members, group_members, project_members, user_roles)A red toast in the app, the browser console, or the Network tab response
code: "42P17"The JSON body of a failed /rest/v1/... request
A team page, member list, dashboard or admin screen that spins forever or shows nothingThe live app, usually right after adding roles, teams or "admins can see everything"
Other tables suddenly failing tooAny table whose policy also checks the members table

Why it happens so often in Lovable apps

It almost always appears right after you ask for something like:

  • "Only members of a team can see the other members"
  • "Admins can see and edit all profiles"
  • "Users can see projects they've been invited to"

The quickest-looking way to write those rules is a policy on team_members that says "you can see a row if you're in team_members for that team", or a policy on profiles that says "you can see everything if your row in profiles has role = 'admin'". Both ask the table to check itself, and that's the loop.

Step 1: confirm which table and policy (5 minutes)

  1. Open the live app, press F12 (Cmd + Option + I on a Mac) and click Network.
  2. Repeat the action that fails and click the red request to .supabase.co/rest/v1/....
  3. In Response, note the table name in relation "...". That's the table whose policy loops.
  4. In Supabase, open Authentication > Policies (or Database > Policies) and find that table.
  5. Look for a policy whose rule mentions its own table name inside it, for example a team_members policy containing select ... from team_members, or a profiles policy containing select role from profiles.

If the policy on table A checks table B, and the policy on table B checks table A, that loops too. Check both.

Step 2: move the check into a helper function

Supabase's own guide recommends this pattern: a security definer function runs as the role that created it, so it can read the table without triggering the policy again (Supabase RLS docs, "Use security definer functions").

Example for a team members table. Adjust table and column names to match yours.

-- 1. A private schema, so the helper can't be called from your public API
create schema if not exists private;

-- 2. The helper: "is the signed-in user a member of this team?"
create or replace function private.is_team_member(p_team_id uuid)
returns boolean
language sql
security definer
stable
set search_path = ''
as $$
  select exists (
    select 1
    from public.team_members tm
    where tm.team_id = p_team_id
      and tm.user_id = (select auth.uid())
  );
$$;

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

-- 3. Replace the looping policy
drop policy if exists "Members can view team members" on public.team_members;

create policy "Members can view team members"
on public.team_members
for select
to authenticated
using ( (select private.is_team_member(team_id)) );

For "admins can see everything" on profiles, the same idea applies: a private.is_admin() function that reads the role, and a policy that says using ( (select private.is_admin()) or id = (select auth.uid()) ). Even better, keep roles in their own user_roles table that only your server can write to, so nobody can make themselves an admin by editing their own profile.

Three things that matter:

  • Keep the helper out of public. Functions in an exposed schema can be called through your API. A private schema plus revoke ... from anon keeps it internal.
  • Always use auth.uid() inside the helper. Never accept a user id as a parameter, or anyone can ask "is user X an admin?" on someone else's behalf.
  • Fix every policy that used the old check. If projects, messages or catches policies also query the members table directly, switch them to the helper too.

Step 3: test it the way your users hit it

  1. Reload the live app signed in as a normal member: the list should load, with no 42P17 in the Network tab.
  2. Sign in as someone who isn't in that team: they should see nothing for it.
  3. Open the app signed out in a private window: nothing private should load.

If step 2 or 3 shows data, the policy is now too loose, so tighten it before you move on.

What not to do

  • Don't turn RLS off to make the error go away. It does make it go away, by making every row readable to anyone with your public key.
  • Don't change the policy to using (true). Same problem, different spelling.
  • Don't keep re-prompting Lovable with "fix the recursion" in a loop. Each try can add another policy on top of the old one. Policies on the same table combine with OR, so one leftover loose policy quietly opens the table. Check the full list of policies on the table after any fix.

Ready-to-paste prompt for Lovable

My app shows "infinite recursion detected in policy for relation [TABLE]" (42P17). Do not disable RLS and do not use using (true). Create a schema called private, and in it a security definer SQL function with set search_path = '' that checks membership or role using auth.uid() only (no user id parameter). Grant execute to authenticated and revoke it from public and anon. Replace every policy that queries [TABLE] from inside a policy on [TABLE] (or on tables whose policies point back to it) with a call to that function, wrapped as (select private.function_name(...)). List every policy on the affected tables before and after, and tell me which ones you dropped.

Still stuck?

If the list still won't load, or fixing one table broke another, submit it to ProvenFix. We find the policy that loops, apply the fix without loosening your security, and re-run the failing screen to show the error is gone. Free to submit, and you see the result before you pay anything.


FAQ (for the page and for search snippets)

What does "infinite recursion detected in policy for relation" mean? A row-level security policy on that table looks up the same table (directly or through another table's policy), so checking the policy requires checking the policy again. Postgres stops it with error 42P17.

Is it safe to just disable RLS? No. That removes the protection on that table for everyone holding your public key. Fix the policy instead.

Why did it start after I added teams or admin roles? Rules like "members can see other members" or "admins can see all profiles" are usually written as a lookup on the same table, which is exactly what causes the loop.

What's the proper fix? Put the membership or role check in a security definer function in a private schema that uses auth.uid(), then call that function from the policy.

Back to Help