Design wechseln

Supabase-Datenbankdesign: Tabellenstruktur, Beziehungen und Row Level Security – vollständiger Leitfaden

Easton editorial illustration: database service control desk

Ich starre auf die rote Warnung im Supabase Dashboard – „RLS not enabled“. Sofort schießen Fragen durch den Kopf: Könnten Nutzerdaten zu Artikeln durchsickern? Stimmt diese Fremdschlüsselbeziehung? Wie baut man eine n:m-Beziehung richtig auf?

Als ich Supabase das erste Mal nutzte, bin ich in viele Fallen gelaufen. Nach dem Erstellen von Tabellen habe ich RLS vergessen – plötzlich konnte jeder alle Daten lesen. Fremdschlüssel waren unsauber modelliert: Nutzer gelöscht, Artikel blieben. Bei n:m-Beziehungen habe ich sogar Arrays ausprobiert – ein totales Desaster.

Nach einigen Monaten habe ich das Muster für Supabase-Datenbankdesign verstanden. Dieser Artikel fasst die Erfahrungen zusammen.

1. Tabellenstruktur: Namenskonventionen in PostgreSQL

1.1 Namenskonvention: snake_case ist der richtige Weg

PostgreSQL hat eine Eigenheit: Ohne doppelte Anführungszeichen werden Bezeichner in Kleinbuchstaben umgewandelt. Mit Anführungszeichen bleibt die Schreibweise exakt erhalten.

Was bedeutet das? Bei CamelCase (UserProfile) müssen Sie überall Anführungszeichen setzen. Das ist mühsam.

Deshalb gilt in der PostgreSQL-Community: snake_case (Unterstrich-Trennung), Plural für Tabellennamen, Singular für Spaltennamen.

-- ✅ Empfohlen
CREATE TABLE users (
  id UUID PRIMARY KEY,
  email TEXT UNIQUE,
  created_at TIMESTAMPTZ
);

1.2 Spaltentypen: Nicht in MySQL-Denkmuster verfangen

Fehler 1: VARCHAR statt TEXT

In PostgreSQL sind TEXT und VARCHAR performancegleich. Der Unterschied: VARCHAR(n) hat eine Längenbegrenzung. Wenn Sie keine brauchen, nutzen Sie direkt TEXT.

Fehler 2: TIMESTAMP statt TIMESTAMPTZ

TIMESTAMP speichert keine Zeitzoneninformation. Server in den USA, Nutzer in China – die Anzeige wird chaotisch. TIMESTAMPTZ konvertiert Zeitzonen automatisch.

Fehler 3: SERIAL statt UUID

SERIAL ist eine autoinkrementierende Ganzzahl. Für Single-Server-Anwendungen okay, in verteilten Systemen entstehen Kollisionen. UUID ist global eindeutig.

2. Drei Beziehungstypen: 1:1, 1:n und n:m

2.1 Eins-zu-eins: UNIQUE reicht

Typisches Szenario: Nutzer und Profil.

CREATE TABLE profiles (
  id UUID PRIMARY KEY,
  user_id UUID UNIQUE REFERENCES users(id) ON DELETE CASCADE,
  bio TEXT
);

Der Schlüssel: user_id UUID UNIQUE. Die UNIQUE-Constraint stellt sicher, dass jeder Nutzer genau ein Profil hat.

2.2 Eins-zu-viele: Der klassische Fremdschlüssel

Autoren und Bücher. Ein Autor kann viele Bücher schreiben.

CREATE TABLE books (
  id UUID PRIMARY KEY,
  author_id UUID REFERENCES authors(id) ON DELETE CASCADE,
  title TEXT
);

Bei Abfragen kann Supabase JS verknüpfte Daten direkt verschachtelt laden.

2.3 Viele-zu-viele: Die Verknüpfungstabelle ist entscheidend

Studierende und Kurse. Ein Studierender kann mehrere Kurse belegen, ein Kurs hat mehrere Studierende.

Lösung: eine Verknüpfungstabelle.

CREATE TABLE enrollments (
  student_id UUID REFERENCES students(id) ON DELETE CASCADE,
  course_id UUID REFERENCES courses(id) ON DELETE CASCADE,
  PRIMARY KEY (student_id, course_id)
);

3. Row Level Security: Die Datenbank als Türsteher

3.1 Die „Standardverweigerung“-Philosophie von RLS

Beim ersten Supabase-Projekt habe ich eine posts-Tabelle angelegt und mit dem anon key vom Frontend abgefragt. Ergebnis: alle Daten kamen zurück. Schock.

Supabase aktiviert Row Level Security (RLS) standardmäßig nicht. Ohne RLS kann jeder mit dem anon key alle Daten lesen und schreiben.

Deshalb die erste Regel: Nach dem Erstellen einer Tabelle sofort RLS aktivieren.

ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

Aktiviert – und fertig? Noch nicht. RLS ohne Policy bedeutet „Zugriff für alle verweigert“. Mindestens eine Policy ist Pflicht.

3.2 Policy-Syntax: USING und WITH CHECK

  • USING: filtert bestehende Zeilen (SELECT, UPDATE, DELETE)
  • WITH CHECK: prüft neue Zeilen (INSERT, UPDATE)

3.3 Vier häufige Policy-Muster

Muster 1: Nutzer greifen auf eigene Daten zu

CREATE POLICY "Users manage own data"
ON posts FOR ALL
TO authenticated
USING (user_id = auth.uid());

Muster 2: Öffentliche und private Daten gemischt

Veröffentlichte Inhalte für alle sichtbar, Entwürfe nur für den Autor.

Muster 3: Mandantenisolation

Teammitglieder sehen nur Daten ihres eigenen Teams.

Muster 4: RBAC-Rollensteuerung

Administratoren haben erweiterte Rechte.

4. RLS-Performance-Optimierung

4.1 Performance-Killer: Subquery pro Zeile

Subqueries in RLS-Policies werden für jede Zeile ausgeführt. Bei 100.000 Zeilen und einer Subquery zur Teamprüfung: Timeout nach 3 Minuten.

4.2 Optimierung 1: Indizes hinzufügen

Spalten, die in RLS-Policies vorkommen, brauchen Indizes.

CREATE INDEX idx_posts_user_id ON posts(user_id);

Offizieller Supabase-Test: ohne Index 450 ms, mit Index 45 ms – Faktor 10.

4.3 Optimierung 2: SECURITY DEFINER-Funktionen

Subqueries in eine Funktion auslagern – sie läuft nur einmal.

CREATE OR REPLACE FUNCTION user_teams()
RETURNS SETOF UUID
LANGUAGE SQL SECURITY DEFINER STABLE
AS $$ SELECT team_id FROM team_members WHERE user_id = auth.uid(); $$;

5. Praxisbeispiele

5.1 Blog-System: Artikel, Kategorien, Tags

Vollständige Umsetzung mit Tabellenstruktur, RLS-Policies und Index-Konfiguration.

5.2 Multi-Tenant-SaaS: Team-Zusammenarbeit

Team-Datenisolation, Zugriff für Teammitglieder, Admin-Rechte.

Zusammenfassung

Kernpunkte:

  • snake_case-Namensgebung
  • UUID als Primärschlüssel, TEXT für Strings, TIMESTAMPTZ für Zeitstempel
  • RLS ist Pflicht
  • Index + SECURITY DEFINER-Funktionen für Performance

FAQ

Muss RLS direkt nach der Tabellenerstellung aktiviert werden?
Ja, das ist eine Sicherheitsregel. Supabase aktiviert RLS standardmäßig nicht – mit dem anon key kann jeder alle Daten lesen und schreiben.
Warum empfiehlt PostgreSQL snake_case?
PostgreSQL wandelt nicht in Anführungszeichen gesetzte Bezeichner in Kleinbuchstaben um. Bei CamelCase müssen Sie überall doppelte Anführungszeichen setzen – umständlich.
Warum sollte man in RLS-Policies kein FOR ALL verwenden?
FOR ALL ist langsamer als vier getrennte Policies. Getrennte Policies ermöglichen PostgreSQL, die Indexnutzung pro Operation zu optimieren.
Wie prüfe ich den RLS-Status?
Im Supabase Dashboard unter Database den RLS-Status jeder Tabelle einsehen. Rot bedeutet: nicht aktiviert.

4 Min. Lesezeit · Veröffentlicht am: 4. Apr. 2026 · Aktualisiert am: 14. Juli 2026

Ähnliche Beiträge

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog