Zum Inhalt

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:

USING (tenant_id = current_setting('app.tenant_id')::UUID)

Supabase-RLS (korrekt):

USING (tenant_id = (auth.jwt() ->> 'tenant_id')::UUID)


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() und auth.user_role() anlegen
  • [ ] RLS-Policies in Supabase von current_setting auf auth.jwt() umstellen
  • [ ] Prisma .env mit Supabase Connection Strings befüllen
  • [ ] End-to-End-Test: Login → JWT → RLS → Datenzugriff verifizieren

Weiterführende Ressourcen