Insights · Engineering · 2026-06-05 · 9 Min Lesezeit

Supabase Row Level Security für Multi-Tenant-SaaS

RLS-Policies, tenant_id, auth.uid() und die Lecks, die Mandantentrennung still aushebeln

Wie du in Supabase mit Row Level Security mehrere Mandanten sauber trennst: Policy-Aufbau, tenant_id-Strategie, typische Sicherheitslecks und wie du die Trennung wirklich testest.

Was Multi-Tenant in einer geteilten Datenbank bedeutet

In einem Multi-Tenant-SaaS teilen sich mehrere Kunden, die Mandanten, dieselbe Datenbank und oft dieselben Tabellen. Die Zeile eines Kunden darf für keinen anderen sichtbar werden. In Supabase, das auf Postgres läuft, ist das Werkzeug dafür Row Level Security. RLS erlaubt dir, pro Tabelle Regeln zu definieren, die festlegen, welche Zeilen ein Nutzer sehen und verändern darf.

Der entscheidende Punkt: RLS ist standardmäßig aus. Solange du es nicht explizit per ALTER TABLE ... ENABLE ROW LEVEL SECURITY einschaltest, sieht jeder authentifizierte Nutzer alles. Eine vergessene Tabelle ist kein Schönheitsfehler, sondern ein offenes Datenleck zwischen Kunden. Mandantentrennung ist deshalb keine Funktion, die du einmal baust, sondern eine Eigenschaft, die auf jeder einzelnen Tabelle gelten muss.

tenant_id als roter Faden

Die gängige Struktur ist eine Spalte tenant_id auf jeder mandantenbezogenen Tabelle. Daneben gibt es eine Zuordnung, welcher Nutzer zu welchem Mandanten gehört, etwa eine Tabelle memberships mit user_id und tenant_id. Über diese Brücke entscheidet die Policy, ob ein Nutzer eine Zeile sehen darf.

Die Alternative ist, die tenant_id direkt in die JWT-Claims des Nutzers zu schreiben. Dann liest die Policy sie ohne zusätzlichen Tabellen-Zugriff aus dem Token. Das ist schneller, verlagert aber die Verantwortung in den Ausstellungsprozess des Tokens. Wechselt ein Nutzer den Mandanten, muss das Token neu ausgestellt werden, sonst greift er mit altem Claim auf den falschen Mandanten zu. Beide Wege funktionieren, sie haben nur unterschiedliche Fehlerquellen.

Eine Policy von innen

Eine Policy ist ein boolescher Ausdruck, den Postgres pro Zeile auswertet. Für Lesezugriff schreibst du eine USING-Bedingung, für Schreibzugriff zusätzlich eine WITH CHECK-Bedingung. auth.uid() liefert die ID des aktuell angemeldeten Nutzers aus dem JWT. Eine typische Lese-Policy prüft, ob es eine Mitgliedschaft gibt, die den Nutzer mit dem Mandanten der Zeile verbindet.

Der häufigste Fehler ist, nur USING zu setzen und WITH CHECK zu vergessen. Dann kann ein Nutzer zwar nur eigene Zeilen lesen, aber beim Einfügen oder Aktualisieren eine fremde tenant_id setzen und so Daten in einen anderen Mandanten schreiben. Lese- und Schreibregel müssen beide die Mandantenzugehörigkeit erzwingen, sonst ist die Trennung halbseitig.

Die Lecks, die niemand sofort sieht

Drei Lecks tauchen immer wieder auf. Erstens die Tabelle ohne aktiviertes RLS, oft eine spät hinzugefügte Hilfstabelle. Zweitens der Service-Role-Key, der RLS bewusst umgeht und versehentlich clientseitig landet, womit jede Policy wirkungslos wird. Drittens Postgres-Funktionen mit SECURITY DEFINER, die mit den Rechten des Erstellers laufen und so an RLS vorbei lesen, wenn sie nicht sorgfältig eingegrenzt sind.

Dazu kommt die fehlende WITH CHECK-Klausel aus dem letzten Abschnitt und Views, die ohne security_invoker die Rechte ihres Besitzers erben statt die des Aufrufers. Keiner dieser Fehler wirft eine Fehlermeldung. Die Anwendung funktioniert, die Daten fließen, und erst ein neugieriger oder böswilliger Nutzer findet die Lücke. Deshalb prüfen wir diese Punkte bei jedem Supabase-Setup einzeln.

Testen, als wärst du der fremde Mandant

Mandantentrennung testest du nicht durch Anschauen, sondern durch Angreifen. Du legst mindestens zwei Mandanten mit je eigenen Nutzern an und schreibst Tests, die als Nutzer A versuchen, Zeilen von Mandant B zu lesen, zu ändern und zu löschen. Jeder dieser Versuche muss leer zurückkommen oder fehlschlagen. Ein Test, der nur den Erfolgsfall prüft, übersieht genau die Lecks, die wehtun.

Praktisch geht das, indem du in der Testumgebung echte JWTs für verschiedene Nutzer ausstellst und die Abfragen über den anon-Client laufen lässt, nicht über den Service-Role-Key. Nur so durchlaufen die Abfragen RLS wie in Produktion. Ergänzend prüfst du mit einer Abfrage über den Systemkatalog, ob wirklich auf jeder Tabelle RLS aktiv ist. Diese Tests gehören in die CI, damit eine neue Tabelle ohne Policy den Build bricht statt still Daten zu lecken. Wie wir das verdrahten, steht unter Ops-Automation.

Wann RLS nicht reicht

RLS trennt Zeilen, aber es ersetzt keine Anwendungslogik. Komplexe Berechtigungen mit Rollen, geteilten Datensätzen oder hierarchischen Mandanten lassen sich zwar in Policies abbilden, werden aber schnell unübersichtlich und langsam, weil jede Policy bei jeder Abfrage läuft. Ab einem gewissen Punkt gehört Teil der Autorisierung in eine bewusst abgesicherte Server-Schicht.

Die ehrliche Einordnung lautet: RLS ist eine starke, datenbanknahe Verteidigungslinie, die du immer brauchst, aber selten als einzige. Kombiniert mit server-seitigen Aufrufen über API-Pipelines und sauberem Schlüsselmanagement ergibt sich eine Trennung, die hält. Wenn du dein bestehendes Supabase-Projekt darauf prüfen lassen willst, melde dich über Kontakt.

Häufige Fragen

Bevor du fragst.

Reicht RLS allein für Mandantentrennung?
RLS ist das Fundament, aber nur wenn es lückenlos auf jeder Tabelle aktiv ist und keine Service-Role-Abkürzung es umgeht. Eine einzige Tabelle ohne Policy reicht für ein Datenleck. Wie wir das systematisch absichern, steht unter Supabase im Stack.
Was ist der Unterschied zwischen tenant_id über die Tabelle und über die JWT?
Du kannst den Mandanten pro Zeile speichern und gegen die User-Mitgliedschaft prüfen, oder die tenant_id direkt in den JWT-Claims mitführen. JWT-Claims sind schneller, aber müssen bei Mandantenwechsel sauber neu ausgestellt werden. Welcher Weg passt, klären wir im Erstgespräch.
Warum sehe ich mit dem Service-Role-Key alle Daten?
Der Service-Role-Key umgeht RLS bewusst, er ist für Server-Code gedacht. Genau deshalb darf er nie in den Browser oder in eine Mobile-App. Liegt er clientseitig, ist die ganze Mandantentrennung wertlos. Server-seitige Aufrufe bauen wir in API-Pipelines ab.
Wie teste ich, ob die Trennung hält?
Du brauchst automatisierte Tests, die als verschiedene Nutzer derselben und fremder Mandanten lesen und schreiben, und die prüfen, dass fremde Zeilen unsichtbar bleiben. Manuelles Durchklicken findet die Lücken nicht. Mehr zu unserem Vorgehen unter Ops-Automation.

Weiterlesen

Verwandte
Artikel.

Tiefer in angrenzende Themen, Architektur, Tools, Praxis.

Konkret werden

Klingt nach
eurem Projekt?

30 Minuten Gespräch, danach weißt du, ob ein vergleichbares Setup für dich Sinn ergibt, und was es realistisch kostet.

Erstgespräch Weitere Artikel