Should I use Postgres Row-Level Security or application-level workspace checks?
It depends on how your application connects to Postgres. If browser or mobile clients query the database directly through PostgREST, Row-Level Security (RLS) is mandatory. If all traffic flows through a server-side TypeScript boundary, explicit membership checks on every query are your primary defense, and RLS serves as defense-in-depth against forgotten WHERE clauses. Combine live session-to-membership resolution with indexed workspace_id policies. supastack enforces server-side workspace guards across web, mobile, and desktop, and provides a free RLS policy generator for Supabase tables.
Anwendungs-Guards und Datenbank-Policies lösen unterschiedliche Fehler
Entwickler betrachten Autorisierung auf Anwendungsebene und Postgres Row-Level Security oft als Gegensätze. In der Praxis fangen sie unterschiedliche Fehler ab: Serverseitige Guards setzen Rollenhierarchien, Entitlements und Audit-Logs durch, bevor Geschäftslogik läuft, während RLS garantiert, dass eine vergessene WHERE workspace_id = ...-Bedingung niemals die Zeilen aller Mandanten ausgibt.
Egal ob du Supabase oder selbst gehostetes Postgres mit Drizzle nutzt: Jede mandanteneigene Tabelle sollte einen NOT NULL-Fremdschlüssel workspace_id tragen. Der Artikel zu Workspace-Grenzen erklärt, warum dieser Scope immer auf dem Server abgeleitet werden muss.
- Anwendungs-Guards prüfen Sitzungsaktualität, Mitgliederrollen (
owner,admin,member) und Feature-Berechtigungen. - Datenbank-RLS-Policies bilden ein hartes Sicherheitsnetz bei direkten Client-Abfragen oder rohen SQL-Helfern.
- Beide Schichten stützen sich auf eine einzige Wahrheitsquelle: die Join-Tabelle
workspace_members.
Prüfe die Live-Mitgliedschaft statt veralteten Cookies zu vertrauen
Eine currentWorkspaceId in einem Browser-Cookie oder Header zu speichern ist praktisch für die Navigation, aber ein Cookie ist eine Präferenz und keine Berechtigung. Wenn ein Admin ein Teammitglied aus einem Workspace entfernt, behält dessen Browser weiterhin das alte Cookie.
Verknüpfe die aktive Sitzung bei jeder Anfrage mit workspace_members. Fehlt die Mitgliedschaftszeile, lehne die Anfrage sofort ab, statt auf das Ablaufen eines JWT oder Cookies zu warten. Der Beitrag über Authentifizierung jenseits des Login-Formulars zeigt, wie Identität in diese Prüfung fließt.
export async function requireWorkspaceViewer(requestedWorkspaceId?: string) {
const session = await requireAuthenticatedSession();
const membership = await db.query.workspaceMembers.findFirst({
where: and(
eq(workspaceMembers.userId, session.user.id),
requestedWorkspaceId
? eq(workspaceMembers.workspaceId, requestedWorkspaceId)
: undefined,
),
with: { workspace: true },
});
if (!membership) throw new ForbiddenError("Not a member of this workspace");
return { user: session.user, workspace: membership.workspace, role: membership.role };
}Schreibe schnelle, indizierte Row-Level-Security-Policies
Naive RLS-Policies, die auth.uid() direkt für jede Zeile aufrufen, zwingen Postgres dazu, die Funktion pro Tupel neu auszuwerten. Das Kapseln von (select auth.uid()) in einer skalaren Subquery erlaubt dem Query-Planner das Caching als initPlan, und ein zusammengesetzter Index auf (workspace_id, user_id) hält Mitgliedschafts-Lookups im Sub-Millisekunden-Bereich.
Mit dem kostenlosen Supabase-RLS-Policy-Generator kannst du sofort ausführbare SELECT-, INSERT-, UPDATE- und DELETE-Policies für mandantenfähige Tabellen erzeugen, ohne Boilerplate-SQL von Hand zu schreiben.
CREATE INDEX IF NOT EXISTS idx_workspace_members_lookup
ON workspace_members (workspace_id, user_id);
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
CREATE POLICY "workspace_members_manage_documents"
ON documents FOR ALL TO authenticated
USING (
workspace_id IN (
SELECT wm.workspace_id FROM workspace_members wm
WHERE wm.user_id = (SELECT auth.uid())
)
)
WITH CHECK (
workspace_id IN (
SELECT wm.workspace_id FROM workspace_members wm
WHERE wm.user_id = (SELECT auth.uid())
AND wm.role IN ('owner', 'admin', 'member')
)
);Häufige Fragen zu RLS und Workspace-Auth
Teams, die mandantenfähiges Postgres verdrahten, stoßen bei Service-Rollen, Hintergrund-Jobs und Performance immer wieder auf dieselben Grenzfälle.
- Umgehen Drizzle ORM oder Prisma RLS? — Ja, wenn sich dein Server mit dem
postgres-Superuser oder dem Supabase-service_role-Key verbindet. Deshalb müssen Server Actions und API-RoutenrequireWorkspaceViewer()aufrufen und jede Abfrage nachworkspaceIdfiltern. - Warum
(select auth.uid())in RLS-Policies in Klammern setzen? — So wertet der Postgres-Optimizerauth.uid()einmal pro Statement statt einmal pro gescannter Zeile aus und vermeidet langsame Sequential Scans auf großen Tabellen. - Wie sollten Hintergrund-Jobs auf Mandantendaten zugreifen? — Übergib die verifizierte
workspaceIdaus der authentifizierten Server-Route im Job-Payload und beschränke Lese- und Schreibzugriffe des Workers auf dieseworkspaceId. - Wie verifiziere ich, dass die Mandantenisolation wirklich hält? — Schreibe automatisierte Negativtests, in denen Nutzer A über Web-, Mobile- und API-Routen die Datensatz-ID von Nutzer B anfragt oder ändert, und prüfe auf
403/404ohne geänderte Zeilen.