Supabase Fehler: "infinite recursion detected in policy for relation" beheben

7. August 2026 ca. 6 Min. Lesezeit Supabase
Inhalt 10

Du hast eine RLS-Policy geschrieben, die auf eine andere Tabelle schaut – und plötzlich knallt Supabase mit dieser Fehlermeldung:

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

In diesem Artikel zeige ich dir, warum diese Rekursion entsteht und wie du sie mit einer sauberen Policy-Struktur (oder SECURITY DEFINER) dauerhaft beseitigst.

Das Problem: Warum RLS in eine Endlosschleife läuft

Postgres prüft bei jeder Zeile die Row Level Security Policies. Wenn Policy A auf Tabelle B zugreift und Policy B wieder auf Tabelle A (oder dieselbe Tabelle), entsteht eine Rekursion – Postgres bricht ab, statt endlos zu prüfen.

Klassisches Beispiel: Eine profiles-Policy prüft, ob der User in team_members ist – und die team_members-Policy liest wieder aus profiles.

-- Problematisch: Policy auf profiles liest 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
  )
);

-- ... und Policy auf team_members liest 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()
  )
);

Die Lösung: Rekursion gezielt vermeiden

Schritt 1: Policy nur auf auth.uid() stützen

Prüfe zuerst, ob du überhaupt eine zweite Tabelle brauchst. Oft reicht:

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);

Schritt 2: Hilfsfunktion mit SECURITY DEFINER

Wenn du Rollen/Teams prüfen musst, kapsle den Lookup in einer Funktion, die RLS umgeht – aber nur den notwendigen Check macht:

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 nutzt die Funktion statt direkten JOIN
CREATE POLICY "profiles_team_select" ON profiles
FOR SELECT USING (
  auth.uid() = user_id
  OR public.is_team_member(id)
);

Wichtig: SET search_path = public setzen und die Funktion so eng wie möglich halten – sonst öffnest du ungewollt Daten.

Schritt 3: Policies testen

-- Als konkreter User testen (SQL Editor)
SELECT set_config('request.jwt.claim.sub', 'USER-UUID-HIER', true);
SELECT * FROM profiles;

Oder im Client mit dem normalen anon/authenticated Key – nicht mit dem Service-Role-Key, der RLS umgeht.

Häufige Fehler

  • Policy ruft dieselbe Tabelle auf: EXISTS (SELECT 1 FROM profiles WHERE …) innerhalb einer Policy auf profiles triggert oft Rekursion.
  • Zwei Tabellen, die sich gegenseitig referenzieren: Immer eine Seite über SECURITY DEFINER oder denormalisierte Spalten (z. B. owner_id) lösen.
  • Service Role im Frontend: Kaschiert den Fehler lokal, bricht in Produktion mit dem Anon-Key.

Vollständiges Arbeitsbeispiel

-- Saubere Variante für Profile + 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. ✅ Welche beiden Policies/Tabellen referenzieren sich?
  2. ✅ Kann der Check nur mit auth.uid() gelöst werden?
  3. ✅ Falls nicht: Hilfsfunktion mit SECURITY DEFINER + engem search_path
  4. ✅ Test mit authenticated Session, nicht mit Service Role
  5. ✅ Nach Fix: alte, rekursive Policies droppen (DROP POLICY …)

Fazit

infinite recursion detected in policy ist kein Bug in Supabase – es ist Postgres, der eine zirkuläre RLS-Prüfung stoppt. Entkopple die Policies (direkt über auth.uid() oder über eine schlanke SECURITY DEFINER-Funktion), und die Fehlermeldung verschwindet.

Weitere Ressourcen

Steckst du noch in einer rekursiven Policy fest? Schreib einen Kommentar – ich schaue gerne mit drauf.

Kommentare