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 aufprofilestriggert oft Rekursion. - Zwei Tabellen, die sich gegenseitig referenzieren: Immer eine Seite über
SECURITY DEFINERoder 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
- ✅ Welche beiden Policies/Tabellen referenzieren sich?
- ✅ Kann der Check nur mit
auth.uid()gelöst werden? - ✅ Falls nicht: Hilfsfunktion mit
SECURITY DEFINER+ engemsearch_path - ✅ Test mit authenticated Session, nicht mit Service Role
- ✅ 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
- permission denied for table (42501)
- PGRST116: multiple (or no) rows
- Supabase: Users-Tabelle abfragen
- Supabase Auth: Invalid login credentials
- Supabase Edge Functions: Invalid JWT / 401
- Supabase Storage 403 RLS beim Upload
- Supabase Edge Functions CORS Fehler beheben
- Supabase Storage File Upload sicher implementieren
- RLS Insert-Fehler: new row violates row-level security policy
- Supabase RLS Dokumentation
Steckst du noch in einer rekursiven Policy fest? Schreib einen Kommentar – ich schaue gerne mit drauf.
Kommentare