Row Level Security

Wie Kit den Datenbankzugriff absichert — Anwendungsschicht-Sicherheitsmodell und optionale Supabase RLS-Richtlinien

Row Level Security (RLS) ist eine PostgreSQL-Funktion, die einschränkt, auf welche Zeilen ein Datenbankbenutzer zugreifen kann. Supabase aktiviert RLS standardmäßig bei neuen Tabellen und empfiehlt, Richtlinien für jede Tabelle zu schreiben.
Kit verfolgt einen anderen Ansatz — es verwendet Sicherheit auf Anwendungsebene statt datenbankbasierter RLS. Diese Seite erklärt warum, wie es funktioniert und wann du möglicherweise zusätzliche RLS-Richtlinien hinzufügen möchtest.

Kits Sicherheitsmodell

Kit sichert den Datenbankzugriff auf der Anwendungsebene mit drei Mechanismen:
  1. Clerk-Authentifizierung überprüft die Identität des Benutzers bei jeder Anfrage über Middleware
  2. Explizite userId-Filterung stellt sicher, dass jede Abfrage nur Daten zurückgibt, die dem authentifizierten Benutzer gehören
  3. Das Repository Pattern erzwingt diese Zugriffsmuster konsistent im gesamten Codebase

Wie es in der Praxis funktioniert

Jede Datenbankabfrage in Kit enthält einen userId-Filter. Es ist für den Anwendungscode unmöglich, versehentlich Daten eines anderen Benutzers zurückzugeben, weil die Abfragefunktionen eine Benutzer-ID als Parameter erfordern:
typescript
// src/lib/db/queries/files.ts — Eigentümerprüfung ist in die Funktionssignatur eingebaut

export async function getUserFileById(
  id: string,
  userId: string   // Aufrufer muss die ID des authentifizierten Benutzers angeben
): Promise<File | null> {
  return prisma.file.findFirst({
    where: {
      id,
      userId,  // Gibt die Datei nur zurück, wenn sie diesem Benutzer gehört
    },
  })
}
Der aufrufende Code in API-Routen und Server Actions übergibt immer die authentifizierte Benutzer-ID von Clerk:
typescript
// In einer API-Route oder Server Action
const { userId } = await getServerAuth()
if (!userId) throw new Error('Unauthorized')

const file = await getUserFileById(fileId, userId)
if (!file) throw new Error('Not found')  // Deckt auch unbefugten Zugriff ab
Dieses Muster wiederholt sich im gesamten Codebase — Subscription-Abfragen filtern nach userId, Credit-Transaktionen filtern nach userId, KI-Konversationen filtern nach userId. Die Sicherheitsgrenze liegt im TypeScript-Code, nicht in SQL-Richtlinien.

Warum Sicherheit auf Anwendungsebene?

Kit hat diesen Ansatz aus drei praktischen Gründen gewählt:
1. Prisma verbindet sich mit dem Service Role Key. Prisma verwendet den DATABASE_URL-Verbindungsstring, der sich als Datenbankbesitzer (oder eine privilegierte Rolle bei Supabase) authentifiziert. Diese Rolle umgeht standardmäßig alle RLS-Richtlinien. Das Hinzufügen von RLS-Richtlinien zu Tabellen würde einen Fehler im Anwendungscode nicht schützen, da Prismas Verbindung unabhängig davon vollen Zugriff hat.
2. Typsicherheit erkennt Fehler zur Kompilierzeit. Jede Abfragefunktion hat einen userId: string-Parameter in ihrer TypeScript-Signatur. Wenn du ihn vergisst, erkennt der Compiler es, bevor der Code ausgeführt wird. Das ist eine stärkere Garantie als RLS, das erst zur Laufzeit fehlschlägt.
3. Einfacher zu verstehen. Die Sicherheitslogik liegt neben der Geschäftslogik in TypeScript, nicht in einer separaten SQL-Schicht. Wenn du eine API-Route liest, siehst du genau, auf welche Daten sie zugreift, ohne Datenbankrichtlinien zu referenzieren.

Wann RLS hinzufügen

Es gibt Szenarien, in denen das Hinzufügen von Supabase RLS-Richtlinien eine sinnvolle zusätzliche Sicherheit bietet:

Direkter Supabase-Client-Zugriff

Wenn du den Supabase JavaScript-Client (nicht Prisma) für Datenabfragen verwendest — beispielsweise bei Supabase Realtime-Subscriptions oder clientseitigen Abfragen — authentifizieren sich diese Anfragen mit dem Anon-Key und unterliegen RLS-Richtlinien. Ohne Richtlinien würden diese Abfragen vollständig blockiert (Supabase aktiviert RLS standardmäßig).
typescript
// Diese Abfrage verwendet den Supabase JS-Client, NICHT Prisma
// Sie respektiert RLS-Richtlinien und verwendet den Anon-Key
import { supabase } from '@/lib/db/supabase'

const { data } = await supabase
  .from('File')
  .select('*')
  .eq('userId', userId)

Multi-Tenant-Anwendungen

Wenn du Kit zu einer Multi-Tenant-Anwendung erweiterst, bei der verschiedene Organisationen dieselbe Datenbank teilen, bietet RLS ein Sicherheitsnetz. Selbst wenn der Anwendungscode einen Fehler hat, verhindert RLS, dass Daten über Mandantengrenzen hinweg lecken.

Defense in Depth

Für hochsensible Daten (Finanzdaten, Gesundheitsdaten, personenbezogene Daten) folgt das Hinzufügen von RLS als zweite Schutzschicht dem Defense-in-Depth-Sicherheitsprinzip. Wenn ein Code-Fehler die anwendungsseitige Prüfung umgeht, blockiert die Datenbank weiterhin unbefugten Zugriff.

Regulatorische Anforderungen

Manche Compliance-Frameworks (SOC 2, HIPAA) können datenbankbasierte Zugriffskontrollen zusätzlich zu anwendungsseitigen Prüfungen erfordern. RLS-Richtlinien liefern prüfbare Nachweise für Datenisolierung.

RLS-Richtlinien hinzufügen

Wenn du dich entscheidest, RLS hinzuzufügen, findest du hier die Einrichtung für Kits Tabellen.

Schritt-für-Schritt-Einrichtung

1

RLS auf der Tabelle aktivieren

RLS ist bei Supabase standardmäßig aktiviert, aber wenn du Tabellen via Prisma-Migrationen erstellt hast, musst du es möglicherweise explizit aktivieren:
sql
-- Im Supabase SQL-Editor ausführen
ALTER TABLE "File" ENABLE ROW LEVEL SECURITY;
2

Hilfsfunktion für Auth erstellen

Eine Funktion erstellen, die die Benutzer-ID aus dem Supabase-JWT-Token extrahiert. Diese wird in allen Richtlinien verwendet:
sql
-- Clerk-Benutzer-ID aus dem Supabase Auth-JWT extrahieren
-- Die Benutzer-ID ist im 'sub'-Claim gespeichert
CREATE OR REPLACE FUNCTION auth.clerk_user_id()
RETURNS TEXT AS $$
  SELECT nullif(
    current_setting('request.jwt.claims', true)::json->>'sub',
    ''
  )
$$ LANGUAGE sql STABLE;
3

SELECT-Richtlinien schreiben

Benutzern erlauben, nur ihre eigenen Daten zu lesen:
sql
-- Benutzer können nur ihre eigenen Dateien lesen
CREATE POLICY "Users can view own files"
  ON "File"
  FOR SELECT
  USING ("userId" = auth.clerk_user_id());

-- Benutzer können nur ihre eigene Subscription lesen
CREATE POLICY "Users can view own subscription"
  ON "Subscription"
  FOR SELECT
  USING ("userId" = (
    SELECT id FROM "User" WHERE "clerkId" = auth.clerk_user_id()
  ));

-- Benutzer können nur ihre eigenen Credit-Transaktionen lesen
CREATE POLICY "Users can view own transactions"
  ON "CreditTransaction"
  FOR SELECT
  USING ("userId" = (
    SELECT id FROM "User" WHERE "clerkId" = auth.clerk_user_id()
  ));
4

INSERT/UPDATE/DELETE-Richtlinien schreiben

Schreibzugriff steuern:
sql
-- Benutzer können nur Dateien für sich selbst erstellen
CREATE POLICY "Users can upload own files"
  ON "File"
  FOR INSERT
  WITH CHECK ("userId" = (
    SELECT id FROM "User" WHERE "clerkId" = auth.clerk_user_id()
  ));

-- Benutzer können nur ihre eigenen Dateien löschen
CREATE POLICY "Users can delete own files"
  ON "File"
  FOR DELETE
  USING ("userId" = (
    SELECT id FROM "User" WHERE "clerkId" = auth.clerk_user_id()
  ));
5

Richtlinien testen

Den Supabase SQL-Editor zur Überprüfung verwenden. Zur anon-Rolle wechseln und testen:
sql
-- JWT-Claims setzen, um einen authentifizierten Benutzer zu simulieren
SET request.jwt.claims = '{"sub": "user_test_clerk_id"}';

-- Dies sollte nur Dateien für den simulierten Benutzer zurückgeben
SET ROLE anon;
SELECT * FROM "File";
RESET ROLE;

Richtlinien-Beispiele für alle Tabellen

RLS mit Prisma kombinieren

Wenn du RLS-Richtlinien hinzufügst, beachte, wie verschiedene Verbindungsmethoden mit ihnen interagieren:
VerbindungRolleRLS angewendet?Anwendungsfall
Prisma (DATABASE_URL)Besitzer / Service RoleNein — umgeht RLSServerseitige Abfragen, API-Routen, Server Actions
Supabase JS (anon-Key)anon-RolleJaClientseitige Abfragen, Realtime-Subscriptions
Supabase JS (service_role-Key)Service RoleNein — umgeht RLSServerseitige Admin-Operationen
Supabase Edge FunctionsHängt vom verwendeten Key abHängt abServerlose Funktionen

Den Anon-Key für RLS-erzwungene Abfragen verwenden

Wenn du Prisma-ähnliche Abfragen benötigst, die RLS respektieren, verwende den Supabase JavaScript-Client mit dem anon-Key:
typescript
import { createClient } from '@supabase/supabase-js'

// Client mit Anon-Key — RLS-Richtlinien werden erzwungen
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

// Diese Abfrage wird durch RLS gefiltert — gibt nur die eigenen Dateien des Benutzers zurück
const { data: files } = await supabase
  .from('File')
  .select('*')

Wann welche Methode verwenden

SzenarioPrisma verwendenSupabase JS (anon) verwenden
Server ComponentsJaNein
API-RoutenJaNein
Server ActionsJaNein
Clientseitige AbfragenNeinJa
Realtime-SubscriptionsNeinJa
Admin-OperationenJaNein

Zusammenfassung

  1. Kit verwendet standardmäßig Sicherheit auf Anwendungsebene — Clerk-Authentifizierung und explizite userId-Filterung in jeder Abfragefunktion.
  2. Prisma umgeht RLS — Das Hinzufügen von Richtlinien ändert nichts am Verhalten deines bestehenden serverseitigen Codes.
  3. RLS hinzufügen, wenn du den Supabase JS-Client direkt verwendest, Multi-Tenant-Features baust oder Defense-in-Depth für Compliance benötigst.
  4. RLS ist optional — Der Ansatz auf Anwendungsebene mit TypeScript-Typsicherheit ist ein gültiges und weit verbreitetes Sicherheitsmodell.