Was RLS ist und warum du es brauchst
Row Level Security ist ein PostgreSQL-Feature, das den Datenbankzugriff auf Zeilenebene steuert. Ohne RLS: Jeder Nutzer mit Datenbankzugriff sieht alle Zeilen. Mit RLS: Du definierst Policies, die bestimmen, welche Zeilen ein Nutzer sehen, erstellen, ändern oder löschen kann, direkt in der Datenbank.
Das ist fundamentaler Unterschied zu anwendungsseitiger Filterung. Wenn du WHERE user_id = current_user_id in deinem Anwendungscode filterst und du einen Bug hast oder jemand die API direkt aufruft, lecken Daten. Mit RLS ist es unmöglich, die Datenbank erzwingt die Regel, egal was der Anwendungscode tut.
Für DSGVO-Compliance in deutschen B2B-Apps ist RLS ein starkes Privacy-by-Design-Argument: Die Datenisolierung ist technisch erzwungen, nicht nur in der Dokumentation beschrieben.
RLS aktivieren und erste Policy schreiben
RLS aktivieren per Tabelle: ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
Ohne Policies wird jetzt jede Abfrage blockiert (kein Zugriff ist der Standard). Erstelle eine SELECT-Policy: CREATE POLICY "users_read_own_projects" ON projects FOR SELECT USING (user_id = auth.uid());
Das erlaubt Nutzern, nur ihre eigenen Zeilen zu lesen. Für INSERT: CREATE POLICY "users_insert_own_projects" ON projects FOR INSERT WITH CHECK (user_id = auth.uid());
Der Unterschied: USING gilt für bestehende Zeilen (SELECT, UPDATE, DELETE). WITH CHECK gilt für neue Zeilen (INSERT, UPDATE). Für UPDATE brauchst du beide:
Multi-Tenant-Policies für B2B-SaaS
Für Multi-Tenancy filterst du auf workspace_id statt user_id. Der Trick: Die workspace_id kommt aus dem JWT-Token des Nutzers.
Erstelle zuerst eine Hilfsfunktion: CREATE OR REPLACE FUNCTION auth.workspace_id() RETURNS uuid AS $$ SELECT (auth.jwt() ->> 'workspace_id')::uuid; $$ LANGUAGE sql STABLE;
Dann deine Policies: CREATE POLICY "workspace_isolation" ON projects FOR ALL USING (workspace_id = auth.workspace_id()) WITH CHECK (workspace_id = auth.workspace_id());
Dieselbe Policy auf allen Tabellen. Das ist das Fundament für DSGVO-konforme Mandantentrennung in deutschen SaaS-Produkten.
Rollenbasierte Policies
Für feingranulare Kontrolle (Admins dürfen mehr als normale Nutzer) erstellst du separate Policies für jede Rolle:
CREATE POLICY "admins_read_all_workspace_data" ON projects FOR SELECT USING ( workspace_id = auth.workspace_id() AND ( auth.jwt() ->> 'role' = 'admin' OR user_id = auth.uid() ) );
Das erlaubt Admins, alle Projekte im Workspace zu sehen, während normale Nutzer nur ihre eigenen sehen. Für Schreiboperationen kannst du ähnliche Policies erstellen, die nur Admins erlauben, fremde Datensätze zu ändern.
Das dreistufige Modell (owner/admin/member), das wir für Mittelstandskunden verwenden, basiert auf diesem Muster.
Performance-Optimierung für RLS
RLS-Policies können Performance-Probleme verursachen, wenn sie nicht korrekt indexiert sind. Jede Policy-Prüfung läuft für jede zurückgegebene Zeile, bei Millionen von Zeilen kann das langsam werden.
Best Practices: Erstelle immer Indizes auf Spalten, die in USING-Klauseln verwendet werden: CREATE INDEX idx_projects_workspace_id ON projects(workspace_id); CREATE INDEX idx_projects_user_id ON projects(user_id);
Verwende STABLE Hilfsfunktionen (wie auth.workspace_id()), die PostgreSQL cachen kann. Vermeide komplexe Subqueries in Policies, erzwinge Joins auf separate Lookup-Tabellen nur wenn nötig.
Mit diesen Optimierungen haben wir RLS-geschützte Tabellen mit 10 Millionen Zeilen in unter 50ms abgefragt.
Häufige Fehler und wie man sie vermeidet
Fehler 1: RLS aktivieren aber keine Policies erstellen. Ergebnis: Alle Abfragen werden blockiert. Immer sofort nach ALTER TABLE ... ENABLE ROW LEVEL SECURITY mindestens eine Basis-Policy erstellen.
Fehler 2: Nur SELECT-Policies schreiben, INSERT/UPDATE/DELETE vergessen. Nutzer können dann keine Daten schreiben. Für jede Operation eine Policy definieren oder eine ALL-Policy verwenden.
Fehler 3: Service Role Key im Frontend-Code verwenden. Der Service Role Key umgeht RLS komplett. Verwende immer den anon-Key im Frontend-Code. Der Service Role Key gehört nur in serverseitige Umgebungen (Xano, Edge Functions).
Fehler 4: Vergessen, Joins in RLS-Policies zu indexieren. Führe EXPLAIN ANALYZE auf deinen häufigsten Queries aus und stelle sicher, dass alle Columns in USING-Klauseln indexiert sind. App Studio führt das bei jedem Produktions-Deployment durch.
RLS-Policies von Grund auf schreiben
Jede RLS-Policy hat drei Komponenten: die Tabelle, auf die sie sich bezieht, die Operation, die sie regelt (SELECT, INSERT, UPDATE, DELETE oder ALL), und den Ausdruck, der für jede Zeile zu wahr oder falsch ausgewertet wird. Die USING-Klausel gilt für Zeilen, die gelesen oder geändert werden, sie prüft bestehende Zeilen. Die WITH-CHECK-Klausel gilt nur für INSERT und UPDATE, sie validiert die neu geschriebene Zeile.
Eine häufige Verwirrungsquelle: Du brauchst separate USING- und WITH-CHECK-Ausdrücke für UPDATE-Policies. USING kontrolliert, welche Zeilen der Nutzer aktualisieren kann (welche Zeilen er finden kann), und WITH CHECK kontrolliert, welche Werte er in diese Zeilen schreiben darf. Wenn Nutzer ihre eigenen Zeilen aktualisieren können sollen, aber nicht das user_id-Feld ändern dürfen, braucht deine UPDATE-Policy: USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id). Ohne das WITH CHECK könnte ein Nutzer eine Zeile, die ihm gehört, aktualisieren und user_id auf die ID eines anderen Nutzers setzen, was den Besitz effektiv überträgt.
Schreibe für komplexe Policies mit Subqueries (Workspace-Mitgliedschaftsprüfungen, Berechtigungstabellen-Lookups) die Subquery zuerst separat aus und teste sie als eigenständiges SELECT, bevor du sie in eine Policy einbettest. PostgreSQLs EXPLAIN ANALYZE ist hier dein Freund, führe es auf einem Policy-Ausdruck aus, um zu sehen, ob er Indizes nutzt oder auf einen sequenziellen Scan zurückfällt.
Häufige RLS-Muster
Muster 1: Nutzer besitzt die Zeile. Das einfachste und häufigste Muster. Die user_id-Spalte referenziert auth.users. USING (auth.uid() = user_id). Funktioniert für Notizen, Dokumente, persönliche Einstellungen.
Muster 2: Team- oder Workspace-Zugriff. Eine workspace_members-Verknüpfungstabelle bestimmt den Zugriff. USING (workspace_id IN (SELECT workspace_id FROM workspace_members WHERE user_id = auth.uid())). Funktioniert für jedes SaaS mit gemeinsam genutzten Ressourcen.
Muster 3: Öffentliches Lesen, Besitzer schreibt. Content-Plattformen, bei denen jeder lesen kann, aber nur der Autor bearbeiten darf. SELECT-Policy: USING (true) oder USING (published = true). UPDATE/DELETE-Policy: USING (auth.uid() = author_id). Funktioniert für Blogs, Verzeichnisse, Marktplätze.
Muster 4: Admin-Bypass. Admins können alle Zeilen in einem Workspace sehen, unabhängig von anderen Eigentumsregeln. Füge eine OR-Bedingung hinzu: USING (auth.uid() = user_id OR auth.uid() IN (SELECT user_id FROM workspace_members WHERE role = 'admin' AND workspace_id = projects.workspace_id)). Verwende dieses Muster sparsam, komplexe OR-Bedingungen sind schwerer zu prüfen und können Performance-Auswirkungen haben.
RLS-Policies testen
Das Testen von RLS ist einer der wichtigsten und am häufigsten übersprungenen Schritte in der Supabase-Entwicklung. Das Ziel ist zu überprüfen, dass Nutzer genau die Zeilen sehen können, die sie sehen sollten, und keine, die sie nicht sehen sollten, mit realistischen Testdaten, die jeden Policy-Zweig durchspielen.
Der schnellste Weg, RLS in Supabase zu testen, ist der Row-Level-Security-Policies-Tester im Supabase-Dashboard. Gehe zu Authentication → Policies → klicke auf "Test Policy" bei einer beliebigen Policy. Gib die UUID eines Nutzers ein und führe eine Abfrage aus, du siehst genau, was dieser Nutzer erhalten würde. Erstelle Testnutzer für jede Rolle in deinem System (Admin, Member, Viewer) und überprüfe deren Zugriff gegen deine Testdaten vor dem Deployment.
Für automatisiertes Testen nutze Supabases JavaScript-Client mit expliziten Nutzer-Sessions. Erstelle einen Testnutzer, melde ihn an, und überprüfe dann, dass Abfragen die erwarteten Zeilen zurückgeben und dass Schreibvorgänge auf nicht autorisierte Zeilen fehlschlagen. Speichere diese Tests zusammen mit deinem Anwendungscode und führe sie in der CI aus. Wir schreiben typischerweise 5 bis 10 RLS-Tests pro Tabelle, die die üblichen Fälle abdecken: eigene Zeilen sichtbar, Zeilen anderer Nutzer unsichtbar, korrekte Rolle für Löschen erforderlich, Insert ohne passenden Besitz blockiert.