Supabase Error: Fix "infinite recursion detected in policy for relation"

7. August 2026 approx. 6 min read Supabase
Contents 10

You wrote an RLS policy that checks another table – and suddenly Supabase throws:

ERROR: infinite recursion detected in policy for relation "profiles"

In this article I'll show you why that recursion happens and how to fix it with a clean policy design (or a SECURITY DEFINER helper).

The Problem: Why RLS Goes Into an Infinite Loop

Postgres evaluates Row Level Security for every row. If policy A reads table B and policy B reads table A again (or the same table), you get recursion – Postgres aborts instead of looping forever.

Classic case: a profiles policy checks team_members, and the team_members policy reads profiles again.

-- Problematic: policy on profiles reads team_members ...
CREATE POLICY "profiles_select" ON profiles
FOR SELECT USING (
  EXISTS (
    SELECT 1 FROM team_members
    WHERE team_members.user_id = auth.uid()
      AND team_members.profile_id = profiles.id
  )
);

-- ... and policy on team_members reads profiles
CREATE POLICY "team_members_select" ON team_members
FOR SELECT USING (
  EXISTS (
    SELECT 1 FROM profiles
    WHERE profiles.id = team_members.profile_id
      AND profiles.user_id = auth.uid()
  )
);

The Solution: Break the Recursion on Purpose

Step 1: Prefer auth.uid() Only

First check whether you need a second table at all. Often this is enough:

CREATE POLICY "profiles_own_rows" ON profiles
FOR SELECT USING (auth.uid() = user_id);

CREATE POLICY "team_members_own_rows" ON team_members
FOR SELECT USING (auth.uid() = user_id);

Step 2: Helper Function with SECURITY DEFINER

When you must check roles/teams, wrap the lookup in a function that bypasses RLS – but only for that narrow check:

CREATE OR REPLACE FUNCTION public.is_team_member(p_profile_id uuid)
RETURNS boolean
LANGUAGE sql
SECURITY DEFINER
SET search_path = public
STABLE
AS $$
  SELECT EXISTS (
    SELECT 1
    FROM team_members
    WHERE profile_id = p_profile_id
      AND user_id = auth.uid()
  );
$$;

-- Policy uses the function instead of a direct join
CREATE POLICY "profiles_team_select" ON profiles
FOR SELECT USING (
  auth.uid() = user_id
  OR public.is_team_member(id)
);

Important: always set SET search_path = public and keep the function as narrow as possible – otherwise you risk leaking data.

Step 3: Test the Policies

-- Test as a concrete user (SQL Editor)
SELECT set_config('request.jwt.claim.sub', 'USER-UUID-HERE', true);
SELECT * FROM profiles;

Or test from the client with the normal anon/authenticated key – not the service-role key, which bypasses RLS.

Common Mistakes

  • Policy queries the same table: EXISTS (SELECT 1 FROM profiles WHERE …) inside a policy on profiles often causes recursion.
  • Two tables referencing each other: resolve one side with SECURITY DEFINER or a denormalized column (e.g. owner_id).
  • Service role in the frontend: hides the bug locally, then fails in production with the anon key.

Complete Working Example

-- Clean pattern for profiles + memberships
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE team_members ENABLE ROW LEVEL SECURITY;

CREATE OR REPLACE FUNCTION public.current_user_team_ids()
RETURNS SETOF uuid
LANGUAGE sql
SECURITY DEFINER
SET search_path = public
STABLE
AS $$
  SELECT team_id FROM team_members WHERE user_id = auth.uid();
$$;

CREATE POLICY "profiles_select" ON profiles
FOR SELECT USING (
  user_id = auth.uid()
  OR id IN (
    SELECT profile_id FROM team_members
    WHERE team_id IN (SELECT public.current_user_team_ids())
  )
);

CREATE POLICY "team_members_select" ON team_members
FOR SELECT USING (
  user_id = auth.uid()
  OR team_id IN (SELECT public.current_user_team_ids())
);

Troubleshooting Checklist

  1. ✅ Which two policies/tables reference each other?
  2. ✅ Can the check be done with auth.uid() only?
  3. ✅ If not: helper with SECURITY DEFINER + tight search_path
  4. ✅ Test with an authenticated session, not the service role
  5. ✅ After the fix: drop the old recursive policies (DROP POLICY …)

Conclusion

infinite recursion detected in policy is not a Supabase bug – it is Postgres stopping a circular RLS check. Decouple the policies (via auth.uid() or a slim SECURITY DEFINER function) and the error goes away.

Additional Resources

Still stuck in a recursive policy? Leave a comment – happy to take a look.

Comments