Short answer
A policy on the table queries a table with RLS on, and that table’s policy ends up running the first policy again. Move the lookup into a security definer function in a private schema, with search_path set to an empty string, and call it from both policies. Don’t fix it by turning RLS off or by writing a policy that lets everyone read.
Why the policies loop, and the safe fix
01
A policy queries its own table, or a table that queries it back
“It occurs when an RLS policy queries a table with RLS enabled, and that table's policies end up triggering the original policy again.” Supabase, Infinite recursion detected in policy for relation
Look at the policies on the table named in the error. A policy on profiles that checks profiles to find the user’s role loops on itself. Two tables whose policies check each other loop too, such as projects checking project_members while project_members checks projects.
02
Sharing and team features are the usual shape
“Two tables whose policies read each other never resolve.” Supabase, Row Level Security
The table that says who belongs to a team, list or workspace is where the loop forms: its policy checks the owner on the main table, and the main table’s policy checks the members.
03
Break the loop with a security definer function
“It reads the membership table as its owner, so the second policy never runs and the cycle is broken” Supabase, Row Level Security
Put the membership lookup in a function that runs as its owner, so it reads the table without running that table’s policy. Create it as postgres in the SQL editor, rewrite both policies to call it, and drop the old policies that caused the loop. This is the shape of Supabase’s example, adapted to a team table:
create schema if not exists private; create function private.user_team_ids() returns setof uuid language sql security definer set search_path = '' stable as $$ select team_id from public.team_members where user_id = (select auth.uid()) $$; revoke execute on function private.user_team_ids() from public; grant usage on schema private to authenticated; grant execute on function private.user_team_ids() to authenticated; create policy "members read teams" on teams for select to authenticated using ( id in (select private.user_team_ids()) ); create policy "members read memberships" on team_members for select to authenticated using ( team_id in (select private.user_team_ids()) );04
Keep the function out of the API
“Never create one in a schema listed under "Exposed schemas" in your API settings.” Supabase, Row Level Security
A security definer function runs with its owner’s rights, so where it lives matters. Create it in a schema that isn’t exposed, such as private, pin search_path, and filter on auth.uid() inside it so it only returns the caller’s rows.
05
Test it as real users, not in the SQL editor
The SQL editor runs as postgres, which skips RLS, so a policy can look fixed there and still fail in the app. We test as a signed-in member, a non-member and a signed-out visitor, and check each sees only what they should.
Checked against the vendor’s docs on : Supabase troubleshooting: Infinite recursion detected in policy for relation; Supabase docs: Row Level Security (avoid recursive policies).
Rather hand it over?
Fixed scope, fixed price, paid upfront. The work happens in writing; no call needed.
- from $450App Fix TicketOne reproducible bug in an existing mobile or web app, diagnosed and fixed.See scope and start
- from $450AI-Built App RescueA Lovable, Bolt or Cursor app checked end to end in 3 days, then its worst problems fixed at a fixed price.No issues worth fixing, no charge. If the Launch Check finds nothing rated medium or higher, Simon refunds the full $450. You keep the report either way.See scope and start
Related: Supabase new row violates row-level security · Supabase RLS enabled but data still exposed · Supabase select returns an empty array · Supabase Signups not allowed for this instance