Fix the 'infinite recursion detected in policy for relation' error in Lovable
The Postgres error 'infinite recursion detected in policy for relation' means one of your Supabase Row Level Security policies queries the same table it protects. Checking the policy forces Postgres to re-check the policy, forever. The reliable fix is a SECURITY DEFINER helper function that reads the table without re-triggering RLS — or a flatter, non-recursive policy. Here is exactly how to do both.
By Founder Name · Last verified: 2026-06-25
What does 'infinite recursion detected in policy for relation' actually mean?
It is Postgres error code 42P17. It fires when a Row Level Security policy on a table contains a subquery that reads the same table the policy is protecting. To decide if a row is visible, Postgres must run the policy. But the policy itself does a SELECT on that table, which triggers the policy again, which does the SELECT again. Postgres detects the loop and aborts instead of hanging.
In a Lovable app this almost always shows up after the AI generates a multi-tenant or role-based access pattern. A classic trigger: a policy on a 'profiles' or 'team_members' table that says 'you can read this row if you are an admin' — and it checks whether you are an admin by selecting from that very same table. That self-reference is the recursion.
The error is not random and it is not a Lovable bug. It is a correct, deterministic safety check from Postgres. The same SQL would fail on any Supabase project. That is good news: the fix is structural and permanent, not a credit-burning guessing game.
| What you see | What it means | First move |
|---|---|---|
| infinite recursion detected in policy for relation "profiles" | A policy on profiles selects from profiles | Find the policy on profiles that subqueries profiles |
| Every query to one table returns the error, others work | The recursion is isolated to that one table's policies | Audit only that table's policies |
| Error appeared right after a 'roles'/'admin' prompt | AI added a role-check policy that self-references | Move the role lookup into a SECURITY DEFINER function |
| Worked in preview, fails for real users | Anon/auth role hits the policy; the owner bypassed it | Test as an authenticated non-owner user |
| Two tables reference each other's policies | Indirect (mutual) recursion across tables | Break the cycle with a helper function on one side |
Why did Lovable generate a policy that causes this?
Lovable's AI writes RLS policies from natural-language intent like 'only admins can edit team settings.' To express 'is this user an admin,' it often inlines a subquery against the same table — the simplest-looking SQL that matches the prompt. It reads cleanly but is recursive. The AI optimizes for the immediate prompt, not for how Postgres evaluates policies against themselves.
This is a textbook case of context rot at file 6-7: by the time the AI is layering role logic onto an existing schema, it has lost sight of the policies it wrote earlier. It generates a fresh self-referencing check rather than reusing a safe lookup pattern. A second related failure mode is false-fixed hallucination — you report the recursion, the AI replies 'Fixed the policy,' and it simply rewrites the same recursive shape with different column names. The error returns on the next real query.
If you have clicked Fix three or more times on this exact error and it keeps coming back, stop. You are in the Bug Doom Loop. Each attempt spends a credit and regenerates a policy with the same flawed structure. The recursion will not resolve until the role lookup is moved out of the policy body.
What is the recursive policy actually doing? (with the failing SQL)
Here is the canonical pattern that produces this error. A 'profiles' table holds a role column, and the SELECT policy tries to grant admins access by checking the profiles table from inside the profiles policy. Postgres must evaluate the policy to run the subquery, and must run the subquery to evaluate the policy — the loop Postgres aborts on.
The broken policy looks like this:
create policy "Admins can view all profiles" on public.profiles for select using ( exists ( select 1 from public.profiles p where p.id = auth.uid() and p.role = 'admin' ) );
The using() expression selects from public.profiles — the same table the policy guards. The moment any role (anon or authenticated) queries profiles, Postgres tries to apply this policy, which requires reading profiles, which requires applying this policy. It detects the cycle and raises 42P17 instead of looping forever.
The same trap appears with a 'team_members' table where membership is checked against team_members, or two tables whose policies each query the other (mutual recursion). The shape is identical: the policy needs data from a table whose access is gated by that same policy.
How do I fix it with a SECURITY DEFINER function? (the reliable fix)
Move the role or membership lookup into a SECURITY DEFINER function. Such a function runs with the privileges of its owner and bypasses RLS on the tables it reads, so the lookup no longer re-triggers the policy. The policy then calls the function instead of selecting from the protected table directly. This is the standard, Supabase-recommended way to break RLS recursion.
The helper function and the rewritten policy:
-- 1. Helper runs as owner, so it does NOT re-trigger RLS on profiles create or replace function public.current_user_role() returns text language sql stable security definer set search_path = public as $$ select role from public.profiles where id = auth.uid() $$; -- 2. Recreate the policy to call the function, not subquery the table drop policy if exists "Admins can view all profiles" on public.profiles; create policy "Admins can view all profiles" on public.profiles for select using ( public.current_user_role() = 'admin' );
Because current_user_role() is SECURITY DEFINER, its internal select from profiles does not invoke the profiles policy — the recursion is broken. Mark it stable so Postgres can cache the result within a statement, and always pin search_path to prevent a malicious schema from shadowing your table. For role checks specifically, the cleaner long-term pattern is a separate user_roles table so role data never lives in the same table its policy protects.
- Open the Supabase SQL Editor for your project (or the Lovable database/SQL panel).
- Create a SECURITY DEFINER function that returns the current user's role, reading the table directly.
- Lock the function down: set a fixed search_path and revoke broad execute grants as needed.
- Drop the recursive policy and recreate it so its using() clause calls the function instead of subquerying the table.
- Re-run the failing query as a normal authenticated user to confirm the error is gone and access is still correct.
Related: Lovable Supabase RLS permissions guide · RLS and auth best practices
Is there a fix that avoids a function entirely?
Yes, when the role data does not have to live in the protected table. The cleanest non-recursive design moves roles into their own table — for example user_roles(user_id, role) — and writes the profiles policy against user_roles instead of profiles. Because the policy now reads a different table, there is no self-reference and no recursion, with or without a helper function.
The restructured schema and policy:
-- Separate table holds roles, so the policy never reads its own table create table if not exists public.user_roles ( user_id uuid references auth.users(id) on delete cascade, role text not null, primary key (user_id, role) ); alter table public.user_roles enable row level security; -- profiles policy references user_roles, not profiles — no recursion create policy "Admins can view all profiles" on public.profiles for select using ( exists ( select 1 from public.user_roles ur where ur.user_id = auth.uid() and ur.role = 'admin' ) );
Give user_roles its own policies (typically: a user may read their own roles; only a service role or admin function may write them). This separation is the most robust fix because role escalation is harder, audits are simpler, and the recursion class of bug cannot reappear on that table. For many apps the SECURITY DEFINER function and a dedicated roles table are used together.
How do I confirm the recursion is fully gone and access is still correct?
Reproducing the error from the owner account will not work — the project owner often bypasses RLS. You must verify as a normal authenticated user, and confirm that the fix did not silently open the table to everyone. Test both that the error is gone and that the access rules still hold for non-admins.
A fix that removes the error but exposes every row is worse than the recursion — that is exactly the misconfiguration that lets users see each other's data. Verifying as a non-owner user is the only way to catch it. If a non-admin can read rows they should not, the policy logic is wrong even though the recursion is gone.
- In the Supabase SQL Editor, run a SELECT against the previously failing table and confirm no 42P17 error is returned.
- Sign into your deployed app as a regular (non-admin) authenticated user and load the screen that was erroring.
- Confirm a non-admin sees only their own rows — not every row — proving the policy still restricts access.
- Sign in as an admin and confirm admin-level access still works as intended.
- Run a query as the anon role (logged out) and confirm it is denied or limited, not erroring and not wide open.
- Check the Supabase logs for any remaining policy errors after exercising each path.
When should I stop and bring in an engineer?
If the recursion returns after every Fix attempt, if two or more tables reference each other and you cannot find the cycle, or if removing the error risks exposing data, the access model needs a structural rewrite — not more prompting. A senior engineer can map every policy, install SECURITY DEFINER helpers, separate roles cleanly, and verify access as real users, then hand you the SQL and an explanation.
Signs to escalate: the same 42P17 error reappears after three or more Fix attempts; the error spans multiple related tables (mutual recursion); or you are unsure whether your 'fix' has quietly made the table public. RLS is the boundary between your users' private data and the open internet — guessing at it is the riskiest place to keep iterating with AI prompts.
Because a broken or over-permissive RLS policy is a live data-exposure risk, we treat it as urgent. An emergency review maps your policies, applies the recursion fix, audits for the can-see-each-other's-data class of bug at the same time, and leaves you with working, documented SQL you fully own.
Related: Lovable security audit service · Book an urgent RLS review
Frequently asked questions
What does 'infinite recursion detected in policy for relation' mean in my Lovable app?
Is this error a bug in Lovable or Supabase?
Why does clicking Fix keep regenerating the same recursion error?
What is a SECURITY DEFINER function and why does it fix RLS recursion?
Is it safe to use SECURITY DEFINER, or does it create a security hole?
Can I fix the recursion without writing a function?
Why does the error only show up for my real users and not when I test it?
After I fix the recursion, how do I make sure I did not expose everyone's data?
Two of my tables reference each other and both error — what is happening?
Can you fix this for me and check the rest of my RLS at the same time?
App down or leaking data? Get an expert on it within 24–48h.
Book a free 30-minute audit call. We'll diagnose what's wrong and tell you exactly what it costs to fix.