Skip to content
VibeSafeDaily

Over-open rules

Every team or family page fails with infinite recursion detected in policy

Cataloged October 22, 2026 · Lovable · Bolt · Cursor · Replit · v0 · Supabase

Symptom

You add teams, families, groups, or workspaces, and suddenly the members list, the group page, and anything shared stop loading. The browser shows a 500 or a generic failed-to-load message, and the real error in the API response or Postgres logs is 42P17: infinite recursion detected in policy for relation members.

Root cause

A Row Level Security policy runs on every row a query touches, and any subquery inside that policy is filtered by RLS too. When the select policy on a membership table asks the same membership table who belongs to a group, Postgres has to run that policy again to answer, and it stops with 42P17. Supabase's Row Level Security guide describes the same failure for two tables whose policies read each other, such as lists and list_members: the pair never resolves and the query fails for every role the policies apply to. Because the policies on groups, projects, or posts usually check the membership table as well, one bad policy takes down every shared screen at once. zacaroto/go_pesca issue 57, opened October 21, 2026, reports a group_members policy that queries group_members, which also broke the groups and catches policies that read it. DaisukeHori/homegohan-app issue 1257, opened October 7, 2026, reports that a signed-in family representative got 42P17 on family_members, family_groups, and family_invites, so the family screens could not load. ronaldrosejoseph/task-sphere issue 40, opened August 29, 2026, reports every read that touched workspace membership failing. PKUSO-WebApp/pkuso-web issue 118, opened August 14, 2026, reports an admin update policy on profiles that queried profiles, so every member's save of their own profile returned a 500 while the page showed a misleading No API key found in request. EIRMIS-org/eirmis issue 27, opened September 17, 2026, reports the two-table form: events and checkin_assignments read each other, and so did guests and invitations. VitorMRodovalho/meridianiq issue 219, opened August 28, 2026, shows why it hides in testing: the app read through the service role key, which bypasses RLS, so the policies had never actually run for a signed-in user. The error fails closed, so nothing leaks, but the tempting quick fixes do leak: disabling RLS, or replacing the policy with using (true), hands every member row to anyone holding the anon key. The fix the guide documents is to move the membership lookup into a security definer function with set search_path = '', so it reads the table as its owner and the policy never re-enters itself, and to have it return only the caller's own memberships. The guide also warns that a security definer function in an exposed schema is callable over the Data API with its creator's privileges, so the helper belongs in a private schema, with execute revoked from public and granted only to authenticated.

Before and after

The before policy asks group_members about group_members, so evaluating it re-enters itself. The after policy asks a private security definer helper that returns only the caller's group ids.

Before

using (group_id in (select group_id from group_members where user_id = auth.uid()))

After

using (group_id in (select private.my_group_ids()))

Fix prompt

In this project and its own Supabase project only. Do not test against any other app or project.

1. Find the loop. Run this in the SQL editor and list every policy whose qual or with_check mentions a table name:
   select tablename, policyname, cmd, qual, with_check
   from pg_policies
   where schemaname = 'public'
   order by tablename;
   Flag any policy on a table that selects from that same table (for example a policy on group_members containing from group_members), and any pair of tables whose policies read each other.

2. Do not fix it by disabling RLS, by dropping the policy, or by replacing it with using (true). Each of those makes every membership row readable to anyone with the anon key.

3. Create a private schema and a membership helper that only returns the caller's own memberships. Use the app's real table and column names:
   create schema if not exists private;
   create or replace function private.my_group_ids()
   returns setof uuid
   language sql
   security definer
   set search_path = ''
   stable
   as $$
     select group_id from public.group_members
     where user_id = (select auth.uid())
   $$;
   revoke execute on function private.my_group_ids() from public;
   grant usage on schema private to authenticated;
   grant execute on function private.my_group_ids() to authenticated;
   Do not put this function in the public schema or any schema listed under Exposed schemas in the API settings. Add a second helper the same way if some policies need a role check, for example private.my_admin_group_ids() returning groups where the caller's role is admin.

4. Rewrite each flagged policy to call the helper instead of querying the table directly, keeping the same policy name and operation, and always naming the role:
   drop policy if exists "Members can view group members" on public.group_members;
   create policy "Members can view group members"
   on public.group_members for select
   to authenticated
   using ( group_id in (select private.my_group_ids()) );
   Do the same for the parent table (groups) and every child table (posts, invites, and so on) whose policies read the membership table. For an admin check on the profiles table, use a helper that reads profiles for the caller only, never an inline exists (select ... from profiles).

5. Put the helpers and rewritten policies in a new migration file. Do not edit migrations that already ran.

6. If any screen reads these tables through the service role key, it never ran the policies. Test with a signed-in user's session instead.

7. Add a pgTAP test under supabase/tests/ that, as a signed-in member, selects from each rewritten table and gets rows back with no 42P17; as a signed-in non-member gets zero rows; and as anon gets nothing.

Do not print tokens, keys, or user rows. Do not edit unrelated files.

Paste this into the coding tool that has your own project open.

2-minute self-check

These steps are for an app you own. Do not run them against someone else's project.

  1. On your own app, sign in as a normal user who belongs to a group, team, or family, open the shared pages, and watch the browser's network tab. Any 500 response whose body says 42P17 or infinite recursion detected in policy is this failure.
  2. In your own Supabase project's SQL editor, run select tablename, policyname, qual from pg_policies where schemaname = 'public'; and look for a policy whose qual selects from the same table it protects, or two tables whose policies read each other.
  3. If you already fixed it, check how. Run select tablename, policyname, qual from pg_policies where qual = 'true'; and confirm no membership or profile table was opened up with using (true), and that RLS is still enabled on each one in Table Editor.

Common questions

Why did adding teams or groups suddenly break every shared page?

The select policy on the membership table almost certainly queries the membership table itself. RLS also filters the subquery inside a policy, so Postgres has to run the same policy again to answer it and stops with 42P17, infinite recursion detected in policy. Policies on groups, posts, and invites usually read the membership table too, so they all fail together. In ronaldrosejoseph/task-sphere issue 40, every read that touched workspace membership failed this way.

Can I just turn off RLS on the members table to make the error go away?

That stops the error by letting anyone with your anon key read every membership row, and often the profiles and groups attached to them. The 42P17 error fails closed, so nothing is leaking while you fix it properly. Supabase's Row Level Security guide breaks the loop with a security definer function that reads only the caller's own memberships, created in a private schema rather than an exposed one, with execute granted only to authenticated.

Sources

Same symptom

Same platform