Row Level Security (RLS) ist eine PostgreSQL-Funktion, die Zeile für Zeile entscheidet, welche Zeilen ein Nutzer lesen oder ändern darf, und die Regel läuft in der Datenbank selbst. In einer Supabase-App ist RLS das Einzige, was Ihre Tabellen von jedem Besucher trennt, der den öffentlichen Schlüssel aus Ihrem Browser kopiert. Hat eine Tabelle kein RLS, kann jeder sie über die öffentliche API lesen oder ändern.
Dieser Leitfaden richtet sich an Menschen, die Web-Apps mit einem KI-Baukasten bauen und verstehen wollen, was die generierten Policies tun, damit sie sie prüfen können. Er behandelt die vier Policy-Typen, die üblichen Muster, Storage und einen Test mit zwei Konten, der zehn Minuten dauert.
Warum RLS Pflicht ist, wenn der Schlüssel im Browser liegt
Eine Supabase-Web-App spricht direkt aus dem Browser mit der Datenbank, die Projekt-URL und der öffentliche Schlüssel (anon oder publishable) stecken also im JavaScript, das jeder Besucher herunterlädt. Das ist so gewollt: Der Schlüssel identifiziert Ihr Projekt, er schützt es nicht. Den Schutz liefern die Policies an jeder Tabelle.
Die Supabase-Dokumentation beschreibt eine Policy als zusätzliche WHERE-Bedingung bei jeder Abfrage und hält fest: Sobald RLS auf einer Tabelle aktiv ist, sind über die API mit dem öffentlichen Schlüssel keine Daten erreichbar, bis Sie Policies anlegen. Gefährlich ist der umgekehrte Fall: Eine Tabelle im public-Schema ohne RLS ist offen. Jeder kann die Browser-Konsole öffnen, Ihre API mit Ihrem eigenen öffentlichen Schlüssel aufrufen und darauf select, insert, update oder delete ausführen.
Der geheime Schlüssel (Service Role) ist etwas anderes. Er umgeht RLS und darf deshalb nie im Client-Code auftauchen. Geheimnisse gehören in Servercode wie Edge Functions. Fehlerhafte Zugriffskontrolle, bei der ein Nutzer an fremde Daten kommt, steht auf Platz eins der OWASP Top 10.
Was ohne RLS passiert
Ohne RLS funktioniert die App in Ihren eigenen Tests weiterhin, deshalb bleibt das Problem unbemerkt. Die Screens fragen nur nach Ihren eigenen Zeilen. Der API ist egal, wonach die Screens fragen.
- Ein Besucher listet jede Zeile der Tabelle mit Bestellungen, Kunden oder Nachrichten auf.
- Ein angemeldeter Nutzer ändert Preis, Rolle oder Besitzer einer Zeile, die jemand anderem gehört.
- Ein Skript löscht eine ganze Tabelle, ganz ohne Konto.
Einen Button in der Oberfläche auszublenden schützt nichts, denn die API akzeptiert jede Anfrage, die der Schlüssel erlaubt. Wenn Sie Backends vergleichen: Unser Leitfaden zu Supabase vs. Firebase zeigt, wie beide mit Zugriffsregeln umgehen.
Die vier Policy-Typen: select, insert, update, delete
Eine Policy gilt für genau eine Operation, und jede Operation nutzt eine oder zwei Bedingungen. USING filtert die bereits vorhandenen Zeilen. WITH CHECK prüft die Zeilen, die geschrieben werden. Die Supabase-Dokumentation empfiehlt eine Policy pro Operation.
| Operation | Verwendete Bedingung | Was sie steuert |
|---|---|---|
| SELECT | USING | Welche vorhandenen Zeilen der Nutzer lesen darf |
| INSERT | WITH CHECK | Ob die neue Zeile erlaubt ist |
| UPDATE | USING und WITH CHECK | Welche Zeilen bearbeitet werden dürfen und was sie werden dürfen. Es braucht außerdem eine SELECT-Policy |
| DELETE | USING | Welche Zeilen gelöscht werden dürfen |
Hier ein vollständiges Beispiel für eine Tabelle todos, in der jede Zeile eine Spalte user_id hat. Zuerst schalten Sie RLS ein:
alter table public.todos enable row level security;Dann eine Policy pro Operation, jeweils mit der TO-Klausel auf angemeldete Nutzer beschränkt:
create policy "Read own todos"
on public.todos for select
to authenticated
using ( (select auth.uid()) = user_id );
create policy "Create own todos"
on public.todos for insert
to authenticated
with check ( (select auth.uid()) = user_id );
create policy "Update own todos"
on public.todos for update
to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );
create policy "Delete own todos"
on public.todos for delete
to authenticated
using ( (select auth.uid()) = user_id );Zwei Details zählen. Die TO-Klausel nennt die Rolle: Ohne sie wird die Policy auch für anonyme Anfragen (die Rolle anon) ausgewertet. Und auth.uid() liefert die ID des angemeldeten Nutzers oder null, wenn niemand angemeldet ist. Dann schlägt der Vergleich still fehl, und keine Zeile passt. Wenn Sie den Aufruf als (select auth.uid()) schreiben, berechnet Postgres ihn einmal pro Anweisung statt einmal pro Zeile, was Supabase aus Performance-Gründen empfiehlt.
Muster 1: Zeilen, die einem Nutzer gehören
Das Nutzer-Muster besteht aus einer Spalte user_id, die mit der ID des angemeldeten Nutzers gefüllt wird, plus den vier Policies oben. Es passt zu Notizen, Aufgaben, Profilen, Einstellungen und allen privaten Daten. Ein nützlicher Standardwert für die Spalte ist auth.uid(), damit der Browser sie nicht mitsenden muss:
user_id uuid not null default auth.uid()
references auth.users (id) on delete cascadeLegen Sie einen Index auf die Spalte, nach der Ihre Policies filtern, denn die Policy läuft bei jeder Abfrage:
create index todos_user_id_idx on public.todos (user_id);Muster 2: Teams und Organisationen
Für gemeinsame Daten speichern Sie die Mitgliedschaft in einer eigenen Tabelle, und die Policy fragt, ob der Nutzer zum Team der Zeile gehört. Eine Tabelle members mit team_id und user_id genügt:
create policy "Members read team projects"
on public.projects for select
to authenticated
using (
team_id in (
select team_id from public.team_members
where user_id = (select auth.uid())
)
);Schreib-Policies sind in den meisten Apps strenger als Lese-Policies: Mitglieder dürfen lesen, nur manche dürfen einfügen oder löschen. Setzen Sie eine Spalte role auf team_members und prüfen Sie sie in den Policies für insert, update und delete. Legen Sie auf team_members einen Index auf user_id an, denn ein zusammengesetzter Primärschlüssel, der mit team_id beginnt, indiziert user_id nicht.
Muster 3: Admin-Rollen
Eine Admin-Policy sollte die Rolle von einem Ort lesen, den der Nutzer nicht bearbeiten kann. Die Supabase-Dokumentation warnt, dass raw_user_meta_data vom Nutzer geändert werden kann und deshalb nicht für Autorisierung taugt, während raw_app_meta_data (app_metadata im JWT) vertrauenswürdig ist. Der einfachere und häufigere Weg ist eine Tabelle mit Admins, in die nur der Datenbankbesitzer schreibt:
create policy "Admins read all invoices"
on public.invoices for select
to authenticated
using (
exists (
select 1 from public.admins
where user_id = (select auth.uid())
)
);Policies für dieselbe Operation werden mit OR verknüpft, diese Policy ergänzt also den Admin-Zugriff zusätzlich zur Nutzer-Policy. Sichern Sie die Tabelle admins mit eigenem RLS und ohne insert-Policy ab, sonst könnte sich ein Nutzer selbst zum Admin machen.
Storage-Policies für hochgeladene Dateien
Dateien in Supabase Storage werden durch RLS-Policies auf der Tabelle storage.objects geschützt, und ein Bucket nimmt keinen Upload an, bis Sie eine Policy anlegen. Üblich ist, die Dateien jedes Nutzers in einem Ordner mit seiner Nutzer-ID abzulegen, und die Policy prüft den ersten Ordner des Pfads:
create policy "Upload to own folder"
on storage.objects for insert
to authenticated
with check (
bucket_id = 'documents'
and (storage.foldername(name))[1] = (select auth.uid())::text
);
create policy "Read own files"
on storage.objects for select
to authenticated
using (
bucket_id = 'documents'
and (storage.foldername(name))[1] = (select auth.uid())::text
);Das Überschreiben einer Datei (Upsert) braucht insert-, select- und update-Policies zusammen. Objekte in einem öffentlichen Bucket kann jeder mit dem Link lesen, legen Sie Rechnungen, Verträge und Ausweisdokumente also in privaten Buckets ab. Siehe auch die Supabase-Seite zu Storage Access Control.
So testen Sie Ihre Policies mit zwei Konten
Der einzige Test, der beweist, dass RLS funktioniert, ist: als ein Nutzer anmelden und versuchen, an die Daten eines anderen zu kommen. Der SQL-Editor zählt nicht, denn er läuft mit erhöhten Rechten und ignoriert Ihre Policies. Nutzen Sie die App selbst oder die API mit einer echten Nutzersitzung.
- Legen Sie zwei Konten A und B an und fügen Sie mit jedem einige Zeilen hinzu.
- Prüfen Sie als A, dass die Liste nur die Zeilen von A zeigt.
- Rufen Sie weiterhin als A die API direkt für die Zeilen von B auf (ein select mit der user_id von B, ein update auf die ID einer Zeile von B, ein delete). Jeder Versuch muss nichts liefern oder fehlschlagen.
- Versuchen Sie, eine Zeile mit der user_id von B einzufügen. Die insert-Policy muss das ablehnen.
- Öffnen Sie die App in einem privaten Fenster, abgemeldet, und fragen Sie dieselben Tabellen ab. Private Tabellen müssen nichts liefern.
- Wiederholen Sie die Schritte für Dateien: A darf die Datei von B über ihren Pfad nicht herunterladen können.
Häufige Fehler
| Fehler | Folge | Lösung |
|---|---|---|
| RLS auf einer Tabelle nie aktiviert | Die Tabelle ist für jeden mit dem öffentlichen Schlüssel offen | RLS auf jeder Tabelle im public-Schema aktivieren |
| Policy ohne TO-Klausel | Wird auch für anonyme Anfragen ausgewertet | to authenticated ergänzen |
| using (true) | Jede Zeile ist für die Rolle sichtbar | Nur für Daten verwenden, die öffentlich sein sollen |
| Update-Policy ohne Select-Policy | Updates ändern stillschweigend nichts | Eine passende Select-Policy ergänzen |
| Rolle aus den Nutzer-Metadaten gelesen | Nutzer können sich die Rolle selbst zuweisen | app_metadata oder eine Tabelle admins verwenden |
| View über einer geschützten Tabelle | Views umgehen RLS standardmäßig | Ab Postgres 15 mit security_invoker = true anlegen |
| Geheimer Schlüssel im Client-Code | Umgeht jede Policy | Nur im Servercode halten |
Wie Cadrant damit umgeht
Cadrant, unser Baukasten, generiert React-Web-Apps auf einem Supabase-Projekt in Ihrem eigenen Supabase-Konto. Legt die KI eine Tabelle an, schreibt sie die Migration mit RLS-Policies für Tabellen pro Nutzer, einschließlich Storage-Policies, und sie kann das aktuelle Schema und die Policies einsehen, bevor sie etwas ändert. Geheime Schlüssel bleiben aus dem Client-Code heraus: Integrationen wie Stripe laufen über Edge Functions, die Geheimnisse liegen als Umgebungsvariablen. Die Einrichtung beschreiben die Dokumentation zur Supabase-Verbindung und der Leitfaden zu KI-Baukästen mit Supabase.
Die KI schreibt die Policies nach Ihren Angaben, also machen Sie Angaben: „Ein Kunde sieht nur seine eigenen Rechnungen, die Geschäftsführung sieht alle.“ Die Prompts in unseren Prompt-Beispielen für eine Web-App zeigen, wie. Generierte Policies sind ein guter erster Entwurf, keine Garantie. Führen Sie immer den Test mit zwei Konten durch, bevor Sie die App mit echten Nutzern teilen. Cadrant kann sich wie jeder KI-Baukasten bei einer Regel irren, und der Test zeigt es Ihnen.