Supabase gibt dir in Minuten eine Datenbank und eine API, und genau das ist die Gefahr: Die API ist vom ersten Moment an offen. Wenn du die Sicherheit nicht konfigurierst, kann jeder mit deinem öffentlichen Key deine Tabellen lesen und beschreiben. Dieser Guide geht in der sinnvollen Reihenfolge alles durch, was du prüfen solltest, bevor du deine App für die Welt öffnest: RLS und Policies mit Beispiel-SQL, welcher Key wohin gehört, Storage, Views, die Auth-Einstellungen und wie du am Ende prüfst, dass wirklich alles dicht ist.
Wie Supabase deine Daten exponiert (und warum das gut ist)
Vor deinem Postgres sitzt eine Schicht (PostgREST), die jede Tabelle im Schema public in einen HTTP-Endpoint verwandelt. Deine App im Browser spricht direkt mit ihr, mit einem öffentlichen Key. Das erspart dir ein Backend, bedeutet aber, dass kein eigener Server dazwischen Berechtigungen prüft: Die Row-Level-Security-Policies sind dein Backend. Sind sie richtig, ist das Design solide; fehlen sie, ist die ganze Tabelle nur eine URL entfernt.
1. RLS auf ALLEN Tabellen aktiviert
Row Level Security ist die wichtigste Barriere von Supabase. Ohne sie kann der öffentliche Key ganze Tabellen direkt aus dem Browser abfragen. Der klassische Vibe-Coder-Fehler: schnell eine neue Tabelle anlegen, oder die KI anlegen lassen, und vergessen, RLS zu aktivieren. Genau das war die Ursache der Lovable-Schwachstelle (CVE-2025-48757).
Prüfe Tabelle für Tabelle im Table Editor: Alle mit „RLS disabled" sind öffentlich zugänglich. Damit du dich nicht aufs Auge verlassen musst, listet diese Abfrage im SQL Editor die fehlenden auf:
select c.relname
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'r' and not c.relrowsecurity;
Und für jede einzelne:
alter table public.tabellenname enable row level security;
Aktivieren allein reicht nicht: Eine Tabelle mit aktivem RLS, aber ohne Policies verweigert alles (deine App funktioniert nicht mehr, bis du sie schreibst), und eine mit einer zu freizügigen Policy schützt gar nichts.
2. Policies, die wirklich abbilden, wer was darf
Eine Policy using (true) für alle lässt die Tabelle auch mit aktivem RLS offen; KI-Assistenten schlagen sie oft vor, „damit es läuft". Schreib konkrete Policies und trenne die Regeln nach Operation, denn manchmal willst du Lesen erlauben, aber nicht Löschen.
Muster A: Jeder Nutzer seine eigenen Zeilen
create policy "notes: eigene lesen" on public.notes
for select to authenticated
using ((select auth.uid()) = user_id);
create policy "notes: eigene anlegen" on public.notes
for insert to authenticated
with check ((select auth.uid()) = user_id);
create policy "notes: eigene ändern" on public.notes
for update to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
Das with check verhindert, dass jemand eine Zeile im Namen eines anderen Nutzers einfügt oder ändert. auth.uid() in ein (select …) zu packen sorgt dafür, dass Postgres es einmal pro Abfrage auswertet statt einmal pro Zeile; bei großen Tabellen ist der Unterschied enorm. Leg einen Index auf user_id.
Muster B: Zugehörigkeit zu einem Team oder einer Organisation
create policy "projects: Teammitglieder" on public.projects
for select to authenticated
using (
exists (
select 1 from public.team_members m
where m.team_id = projects.team_id
and m.user_id = (select auth.uid())
)
);
Die Tabelle team_members braucht ebenfalls eigene Policies, und idealerweise darf nur ein Admin darin einfügen.
Muster C: Öffentlicher Inhalt nur zum Lesen
create policy "posts: öffentlich lesbar" on public.posts
for select to anon, authenticated
using (published = true);
Das ist legitim. Was es nicht geben darf: eine offene Schreib-Policy oder Spalten mit personenbezogenen Daten in einer öffentlich lesbaren Tabelle.
3. anon, service_role und die neuen Keys
Der anon-Key ist öffentlich und gehört ins Frontend: Er ist dafür gedacht, von RLS kontrolliert zu werden. Der service_role-Key umgeht RLS komplett und hat Vollzugriff. Er darf niemals, wirklich niemals, im Browser oder in einer Variable mit NEXT_PUBLIC_, VITE_ oder NUXT_PUBLIC_ auftauchen: Dieses Präfix bedeutet „das wird an den Client geschickt".
Supabase hat außerdem ein neues Key-Paar eingeführt, dessen Namen keinen Zweifel lassen: sb_publishable_… (für den Browser, unterliegt RLS) und sb_secret_… (nur Server, umgeht RLS, separat widerrufbar). Wenn du heute ein Projekt startest, nimm diese; hast du ein älteres, migriere, sobald du kannst. Nutze den geheimen Key nur serverseitig, in Edge Functions oder vertrauenswürdigen Backends, und wenn du einen Leak vermutest, rotiere ihn sofort im Dashboard.
4. Storage: Auch Buckets haben Regeln
Der Dateispeicher nutzt eigene Policies auf der Tabelle storage.objects. Ein öffentlicher Bucket liefert jede Datei per URL an jeden, der sie hat, für immer. Entscheide bewusst, welche Buckets öffentlich sind (Avatare, Produktbilder), und schütze den Rest. Ein Beispiel, damit jeder Nutzer nur in seinem eigenen Ordner lesen und hochladen kann:
create policy "documents: eigener Ordner" on storage.objects
for all to authenticated
using (
bucket_id = 'documents'
and (storage.foldername(name))[1] = (select auth.uid())::text
)
with check (
bucket_id = 'documents'
and (storage.foldername(name))[1] = (select auth.uid())::text
);
Lade private Dokumente in private Buckets und liefere sie über kurzlebige signierte URLs aus, nicht über einen öffentlichen Bucket mit schwer zu erratenden Namen.
5. Views, Funktionen und exponierte Schemas
Drei Stellen, an denen Daten entweichen, selbst wenn die Tabellen in Ordnung sind:
- Views. In Postgres läuft eine View mit den Rechten dessen, der sie erstellt hat; eine View auf eine geschützte Tabelle kann also Zeilen zurückgeben, die die Policies verbieten würden. Erstelle sie mit
with (security_invoker = true)(Postgres 15 oder neuer) oder prüfe jede einzeln. - RPC-Funktionen. Eine
security definer-Funktion läuft mit den Rechten ihres Besitzers. Nutze sie nur, wenn du sie wirklich brauchst, validiere die Parameter darin und beschränke mitrevoke execute … from anon, wer sie aufrufen darf. - Exponierte Schemas. Unter Settings → API wählst du, welche Schemas die API veröffentlicht. Was nicht per HTTP rausgehen soll, gehört nicht auf diese Liste.
6. Auth-Einstellungen, die einen Blick wert sind
- E-Mail-Bestätigung an, damit niemand Konten mit fremden Adressen registriert.
- Rate Limits für Login-Versuche und E-Mail-Versand: Sie sind standardmäßig aktiv; stell sicher, dass sie niemand „zum Testen" hochgesetzt hat.
- Redirect-URLs nur zu deinen Domains erlaubt; eine offene Liste erlaubt es, deinen Login für Weiterleitungen auf Phishing-Seiten zu missbrauchen.
- OAuth-Provider: nur die, die du nutzt, mit dem Consent Screen unter deinem Markennamen.
- Ablauf von Magic Links und OTPs kurz halten; der Standardwert ist vernünftig, verlängere ihn nicht.
7. Secrets und Keys raus aus dem Frontend
Prüfe neben dem geheimen Supabase-Key, dass keine Keys von Drittanbietern (Stripe, E-Mail, KI) im Client-JavaScript stecken. Die KI legt sie manchmal dort ab, damit etwas schnell funktioniert, und sie bleiben für immer im Bundle sichtbar. Aufrufe an kostenpflichtige Dienste laufen über eine Edge Function oder deinen Server, nie aus dem Browser. Siehe die Secrets, die deine App im Browser preisgibt.
8. Umgebungen: Entwicklung ist nicht Produktion
Nutze für die Entwicklung ein separates Supabase-Projekt mit erfundenen Daten. Ein Testprojekt mit einer Kopie der echten Daten und halbfertigen Policies ist genauso exponiert wie die Produktion, und niemand behält es im Auge.
So prüfst du, dass alles dicht ist (der Abschlusstest)
- Inkognito-Test. Kopiere bei geöffneter App aus F12 → Network einen Aufruf an
/rest/v1/und öffne ihn ohne Session. Er muss eine leere Liste oder einen Fehler liefern. - „Mehr verlangen"-Test. Entferne eingeloggt den Filter aus der URL (
?user_id=eq.…). Es dürfen nur deine Zeilen zurückkommen. - Zwei-Konten-Test. Lege zwei Nutzer an und bestätige, dass keiner die Daten des anderen sieht, ändert oder löscht, Storage-Dateien eingeschlossen.
- Security Advisor. Im Dashboard listet der Bereich Advisors Tabellen ohne RLS, freizügige Policies und exponierte Funktionen auf. Halte ihn grün.
Scanaris prüft dein Supabase für dich
Scanaris enthält ein optionales Supabase-Audit, das mit einem schreibgeschützten Connection String Tabelle für Tabelle prüft, ob RLS aktiv ist und ob Policies existieren, und im normalen Scan im Frontend exponierte Keys, freizügiges CORS und Endpoints erkennt, die Daten ohne Session ausliefern. Jeder Fund kommt mit einem Prompt, den du direkt in deine KI einfügen kannst. Lass es vor dem Launch laufen und du weißt, ob du eine Tür offen gelassen hast; schalte das Monitoring ein und es meldet sich, wenn die KI in einer neuen Tabelle wieder eine öffnet.
Häufig gestellte Fragen
Kann ich RLS umgehen, indem ich ein eigenes Backend davorsetze?
Ja, indem du vom Server aus nur den geheimen Key nutzt und den öffentlichen nie im Client. Dann muss dein Backend aber jede Berechtigungsprüfung übernehmen, die RLS machen würde. Für die meisten Apps ist RLS weniger Arbeit und schwerer zu brechen.
Kostet RLS Performance?
Kaum, wenn die Policies gut geschrieben sind: (select auth.uid()) statt auth.uid(), Indizes auf den Spalten, die die Policies nutzen, und keine schweren Unterabfragen. Eine schlecht geschriebene Policy kann eine große Tabelle allerdings ausbremsen.
Was ist mit Tabellen, die die KI nach dem Launch anlegt?
Die kommen ohne RLS zur Welt. Nimm „aktiviere RLS und schreib die Policies" in deine Routine (oder in den Prompt deiner KI) auf, sobald eine neue Tabelle auftaucht, und nutze einen automatischen Re-Scan, um zu merken, wenn es vergessen wird.