DiveLogix360 – Supabase RLS-Strategie¶
Zielgruppe: Entwickler (GEM 01, GEM 02, GEM 12, GEM 13)
Stand: April 2026
Relevant für: NestJS-Backend-Setup, JWT-Hook, Prisma-Anbindung
Warum eine eigene RLS-Strategie für Supabase?¶
Das Datenbankschema verwendet current_setting('app.tenant_id')::UUID als Standard-RLS-Pattern für PostgreSQL. Supabase verwendet jedoch ein eigenes Auth-System das auf auth.jwt() basiert. Diese beiden Ansätze müssen bewusst verbunden werden.
Standard-PostgreSQL-RLS:
Supabase-RLS (korrekt):
Architektur: Zwei DB-Verbindungen¶
NestJS Backend
├── service_role Key → Umgeht RLS vollständig (Admin-Operationen)
│ Nur serverseitig verwenden, nie im Frontend!
└── anon Key → Unterliegt RLS vollständig (Client-Operationen)
Browser / PWA
└── anon Key → Supabase JS Client, RLS aktiv
Faustregel:
- Alle Mutationen (INSERT/UPDATE/DELETE) laufen über das NestJS-Backend mit service_role
- Lesezugriffe können direkt vom Client mit anon Key erfolgen (RLS schützt automatisch)
JWT-Hook: Custom Claims setzen¶
Supabase erlaubt einen custom_access_token_hook der bei jedem Login aufgerufen wird und dem JWT zusätzliche Claims hinzufügen kann.
Setup in Supabase¶
Pfad: Dashboard → Authentication → Hooks → custom_access_token_hook
Hook-Funktion (PostgreSQL)¶
CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event JSONB)
RETURNS JSONB
LANGUAGE plpgsql
STABLE
AS $$
DECLARE
claims JSONB;
v_tenant_id UUID;
v_role TEXT;
BEGIN
-- Bestehende Claims übernehmen
claims := event -> 'claims';
-- tenant_id und role aus users-Tabelle lesen
SELECT tenant_id, role::TEXT
INTO v_tenant_id, v_role
FROM public.users
WHERE id = (event ->> 'user_id')::UUID;
-- Claims erweitern
claims := jsonb_set(claims, '{tenant_id}', to_jsonb(v_tenant_id::TEXT));
claims := jsonb_set(claims, '{user_role}', to_jsonb(v_role));
-- Rückgabe
RETURN jsonb_set(event, '{claims}', claims);
END;
$$;
-- Berechtigungen für den Hook
GRANT EXECUTE ON FUNCTION public.custom_access_token_hook TO supabase_auth_admin;
REVOKE EXECUTE ON FUNCTION public.custom_access_token_hook FROM authenticated, anon, public;
Ergebnis: JWT-Payload nach Login¶
{
"sub": "user-uuid",
"email": "user@example.com",
"tenant_id": "tenant-uuid",
"user_role": "tenant_admin",
"iat": 1234567890,
"exp": 1234568790
}
Hilfsfunktionen für RLS-Policies¶
-- Tenant-ID aus JWT lesen
CREATE OR REPLACE FUNCTION auth.tenant_id()
RETURNS UUID
LANGUAGE SQL STABLE
AS $$
SELECT (auth.jwt() ->> 'tenant_id')::UUID;
$$;
-- Rolle aus JWT lesen
CREATE OR REPLACE FUNCTION auth.user_role()
RETURNS TEXT
LANGUAGE SQL STABLE
AS $$
SELECT auth.jwt() ->> 'user_role';
$$;
RLS-Policies mit JWT-Hilfsfunktionen¶
Sobald der JWT-Hook aktiv ist, können die Policies so formuliert werden:
-- Tenant-Isolation (empfohlen für Supabase)
CREATE POLICY tenant_isolation ON equipment
FOR ALL TO authenticated
USING (tenant_id = auth.tenant_id());
-- Nur tenant_admin und superadmin dürfen löschen
CREATE POLICY delete_only_admin ON customers
FOR DELETE TO authenticated
USING (
tenant_id = auth.tenant_id()
AND auth.user_role() IN ('tenant_admin', 'superadmin')
);
-- Gesperrte Workflows schützen
CREATE POLICY no_update_locked ON usecase_workflows
FOR UPDATE TO authenticated
USING (
tenant_id = auth.tenant_id()
AND is_locked = FALSE
);
Rollen-Mapping: App-Rollen ↔ Supabase¶
DB-Rolle (user_role) |
Supabase-Rolle | Zugriff |
|---|---|---|
superadmin |
service_role (Backend) |
Alle Tenants, kein RLS |
tenant_admin |
authenticated |
Eigener Tenant via RLS |
mitarbeiter |
authenticated |
Eigener Tenant via RLS |
kunde |
authenticated |
Eigener Tenant via RLS (eingeschränkt) |
Wichtig: Supabase kennt nur anon, authenticated und service_role als eingebaute Rollen. Die feingranulare Rollentrennung (tenant_admin vs. mitarbeiter) erfolgt über die JWT-Claims und wird in den RLS-Policies mit auth.user_role() geprüft.
Prisma + Supabase: Connection Strings¶
# backend/.env
# Pooled connection (für NestJS / Prisma Queries)
DATABASE_URL="postgresql://postgres.[ref]:[password]@aws-0-eu-central-1.pooler.supabase.com:6543/postgres?pgbouncer=true"
# Direct connection (für Prisma Migrations)
DIRECT_URL="postgresql://postgres.[ref]:[password]@aws-0-eu-central-1.pooler.supabase.com:5432/postgres"
// backend/prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}
Prisma mit Supabase verbinden (einmalig)¶
cd backend
npx prisma db pull # Schema von Supabase einlesen (Validierung)
npx prisma generate # Prisma Client generieren
RLS aktivieren: Zeitpunkt¶
Wichtig: RLS-Policies mit
auth.jwt()funktionieren erst wenn der JWT-Hook aktiv ist und NestJS die korrekten Tokens weiterleitet.
Reihenfolge:
1. NestJS Auth-Modul implementieren (JWT-Middleware)
2. JWT-Hook in Supabase registrieren und testen
3. Hilfsfunktionen auth.tenant_id() und auth.user_role() anlegen
4. RLS-Policies von current_setting(...) auf auth.tenant_id() umstellen
5. Mit service_role testen (umgeht RLS), dann mit anon-Key verifizieren
Offene TODOs¶
- [ ] JWT-Hook in Supabase registrieren (nach NestJS-Auth-Setup)
- [ ] Hilfsfunktionen
auth.tenant_id()undauth.user_role()anlegen - [ ] RLS-Policies in Supabase von
current_settingaufauth.jwt()umstellen - [ ] Prisma
.envmit Supabase Connection Strings befüllen - [ ] End-to-End-Test: Login → JWT → RLS → Datenzugriff verifizieren