Multi-Tenant Next.js mit Supabase RLS: Production Survival Guide
Warum Supabase RLS für Multi-Tenancy
Deine SaaS-App startet mit drei Mandanten. Supabase RLS Policies sehen bombenfest aus – team_id Checks auf jedem Table, Service Role gesperrt, Middleware mit den richtigen JWT Claims. Dann onboarded Mandant vier, und ein Developer sieht elf Sekunden lang das Dashboard eines anderen Unternehmens, bevor dein Slack explodiert. Separate Datenbanken pro Mandant hätten das umgangen – aber zu $47/Monat pro Postgres-Instance und null Möglichkeit für mandantenübergreifende Analysen, scheitert die Rechnung. Geteilte Schemas klingen vernünftiger, bis deine erste Migration 8.000 Zeilen über zwölf Mandanten sperrt und Support-Tickets sich stapeln. Die Architektur, die in Production wirklich überlebt: geteilte Tables mit Supabase Row Level Security. PostgreSQL-nativ, Sub-Millisekunden-Policy-Checks, und du schreibst die Isolationslogik einmal. Aber RLS in Production hat sieben scharfe Kanten, die wir im Staging nicht sahen – hier ist jede einzelne und die Patterns, die sie flicken.
Warum spielt das überhaupt eine Rolle? Einfach. Dein Daten-Filtering passiert auf Datenbankebene. Falls du bei einer WHERE-Klausel in deiner Next.js API Route Mist baust, sitzt du nachts nicht wach und magst dich vor Datenlecks fürchten, weil die Datenbank selbst dein Sicherheitsnetz ist. Und ehrlich gesagt, heutzutage ist das kein Luxus – es ist eine Notwendigkeit.
Aber lass uns nicht uns selbst etwas vormachen. RLS bringt Overhead in deine Queries, kompliziert Debugging und kann dich bei Migrationen stolpern lassen. Also, wie schneiden verschiedene Multi-Tenancy-Ansätze ab?
| Ansatz | Isolationsstufe | Kosten | Operative Komplexität | Query-Performance |
|---|---|---|---|---|
| Datenbank pro Mandant | Vollständig | Hoch ($50-200/Mandant/Mo) | Sehr hoch | Beste |
| Schema pro Mandant | Stark | Mittel | Hoch (Migrationen) | Gut |
| Geteilte Tables + RLS | Zeilenebene | Niedrig | Mittel | Gut (mit Vorbehalten) |
| Anwendungsebene-Filterung | Keine | Niedrigste | Niedrig | Beste |
Für die meisten SaaS-Produkte mit weniger als 10.000 Mandanten bieten geteilte Tables mit RLS das beste Preis-Leistungs-Verhältnis. Das ist, worauf wir uns hier konzentrieren.

Architektur-Patterns: Shared vs Isolated
Bevor du überhaupt ans Coden denkst, musst du deine Tenant-Auflösungsstrategie wählen. In der Praxis stößt du meistens auf zwei Bestien:
Subdomain-basierte Mandantschaft
Hast du schonmal tenant-slug.yourapp.com gesehen? Willkommen zum häufigsten Pattern für B2B SaaS. Es ist schnittig, professionell, und Tenant-Auflösung in Middleware ist ein Spaziergang.
Pfad-basierte Mandantschaft
Diese hier ist dein Standard-/org/tenant-slug/dashboard. Leichter zu setzen auf, weil keine Wildcard-DNS, und es funktioniert auf Plattformen wie Vercel ohne Custom Domains. Aber lass uns ehrlich sein: es fühlt sich ein bisschen wie Socken zu Sandalen an. Wir empfehlen normalerweise Subdomain-basiert für Production B2B Apps und Pfad-basiert für interne Tools oder MVPs. Später wechseln? Du wirst dein vergangenes Ich verfluchen – diese Patterns zu ändern ist kein Witz.
Aufsetzen des Tenant-Schemas
Hier ist ein Schema-Pattern, das uns über drei verschiedene Production Rollouts nicht im Stich gelassen hat:
-- Core Tenant Table
CREATE TABLE organizations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
plan TEXT NOT NULL DEFAULT 'free',
created_at TIMESTAMPTZ DEFAULT now(),
settings JSONB DEFAULT '{}'
);
-- Membership Junction Table
CREATE TABLE memberships (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
org_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(user_id, org_id)
);
-- Beispiel Tenant-Scoped Table
CREATE TABLE projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
org_id UUID REFERENCES organizations(id) ON DELETE CASCADE NOT NULL,
name TEXT NOT NULL,
description TEXT,
created_by UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ DEFAULT now()
);
-- Index auf org_id — du brauchst ihn auf JEDEM Tenant-Scoped Table
CREATE INDEX idx_projects_org_id ON projects(org_id);
CREATE INDEX idx_memberships_user_id ON memberships(user_id);
CREATE INDEX idx_memberships_org_id ON memberships(org_id);
Die memberships-Tabelle ist der Klebstoff, der alles zusammenhält. Alle deine RLS Policies werden drauf zeigen wie auf ihren Lieblingscousin. User können mehreren Organisationen beitreten, und ihre Rollen bestimmen, was sie dürfen oder nicht. Und hier ist ein kleines Körnchen Weisheit: immer – wirklich, immer – org_id auf jedem Tenant-Scoped Table indexieren. Sonst siehst du deine Queries wie Sirup kriechen, sobald du in Daten schwimmst. Wir wurden überrascht, als der Dashboard eines Kunden von 50ms auf 8 Sekunden mit 100.000 Reihen abstürzte. Lektion gelernt.
RLS Policies, die wirklich skalieren
Hier ist, wo Tutorials normalerweise sich verbeugen und dich stranded zurücklassen. Sie werfen auth.uid() = user_id auf dich und sagen: "Viel Glück!" Aber Multi-Tenant RLS kann nicht so aufgekocht werden.
Das Helper Function Pattern
Warum jede Policy mit Membership Checks zu überladen? Benutze stattdessen eine Helper Function:
-- Helper: Überprüfe, ob aktueller User Mitglied einer Org ist
CREATE OR REPLACE FUNCTION public.is_member_of(org UUID)
RETURNS BOOLEAN AS $$
SELECT EXISTS (
SELECT 1 FROM memberships
WHERE user_id = auth.uid()
AND org_id = org
);
$$ LANGUAGE sql SECURITY DEFINER STABLE;
-- Helper: Hole User's Rolle in einer Org
CREATE OR REPLACE FUNCTION public.get_role_in(org UUID)
RETURNS TEXT AS $$
SELECT role FROM memberships
WHERE user_id = auth.uid()
AND org_id = org
LIMIT 1;
$$ LANGUAGE sql SECURITY DEFINER STABLE;
Warum SECURITY DEFINER? Weil die Funktion mit den Privilegien des Schöpfers läuft und RLS auf der memberships-Tabelle überspringt. Ohne das riskierst du, in eine zirkuläre Abhängigkeits-Kaninchenloch zu fallen, wo RLS auf memberships die Membership-Checks abstürzen lässt, auf die andere Tabellen angewiesen sind.
Und der STABLE Part? Er signalisiert dem Query-Planner, dass der Output der Funktion für die gleiche Input während einer Single Query konstant bleibt, was einige nette Caching-Benefits ermöglicht. Versucht, IMMUTABLE zu verwenden? Nicht machen. Membership kann sich zwischen Transaktionen ändern.
Policies für Tenant-Scoped Tables
Lasst uns Policies für unsere projects-Tabelle anschauen:
-- Enable RLS
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- SELECT: Mitglieder können Projekte ihrer Orgs sehen
CREATE POLICY "Mitglieder können Org-Projekte sehen"
ON projects FOR SELECT
USING (public.is_member_of(org_id));
-- INSERT: Admins und Owners können Projekte erstellen
CREATE POLICY "Admins können Projekte erstellen"
ON projects FOR INSERT
WITH CHECK (
public.get_role_in(org_id) IN ('owner', 'admin')
);
-- UPDATE: Admins und Owners können Projekte aktualisieren
CREATE POLICY "Admins können Projekte aktualisieren"
ON projects FOR UPDATE
USING (public.is_member_of(org_id))
WITH CHECK (
public.get_role_in(org_id) IN ('owner', 'admin')
);
-- DELETE: Nur Owners können Projekte löschen
CREATE POLICY "Owners können Projekte löschen"
ON projects FOR DELETE
USING (
public.get_role_in(org_id) = 'owner'
);
Policies für die Memberships Table selbst
Diese ist knifflig. Die memberships-Tabelle bekommt ihre eigene RLS, aber sie kann die Helper Functions nicht benutzen, weil diese ihrerseits memberships abfragen – Cue zirkuläre Referenz Albträume:
ALTER TABLE memberships ENABLE ROW LEVEL SECURITY;
-- Nutzer können Memberships in Orgs sehen, denen sie angehören
CREATE POLICY "Nutzer können Org-Memberships sehen"
ON memberships FOR SELECT
USING (
org_id IN (
SELECT org_id FROM memberships WHERE user_id = auth.uid()
)
);
-- Nur Owners können Mitglieder hinzufügen
CREATE POLICY "Owners können Mitglieder hinzufügen"
ON memberships FOR INSERT
WITH CHECK (
org_id IN (
SELECT org_id FROM memberships
WHERE user_id = auth.uid() AND role = 'owner'
)
);
Ja, da ist eine Subquery auf der gleichen Tabelle. Und ja, PostgreSQL nagelt es. Die Subquery überprüft deine eigene Membership, unbeeinflusst von der Policy, die gerade definiert wird, weil RLS nur die äußere Query umhüllt. Aber test das – ernsthaft, du willst keinen Bug in Production finden.

Next.js Middleware für Tenant Resolution
Mit Next.js 15 und dem schniekem App Router ist Middleware, die am Edge läuft, der perfekte Hausherr für Tenant Resolution. Hier ist unser bewährtes Pattern für Subdomain-basierte Setups:
// middleware.ts
import { createServerClient } from '@supabase/ssr';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const PUBLIC_ROUTES = ['/login', '/signup', '/invite'];
export async function middleware(request: NextRequest) {
const hostname = request.headers.get('host') || '';
const currentHost = hostname.split('.')[0];
// Skip für Main Domain und localhost
const isMainDomain = currentHost === 'app' || currentHost === 'www' || currentHost === 'localhost:3000';
let response = NextResponse.next({
request: { headers: request.headers },
});
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => {
request.cookies.set(name, value);
response.cookies.set(name, value, options);
});
},
},
}
);
const { data: { user } } = await supabase.auth.getUser();
if (!isMainDomain) {
response.headers.set('x-tenant-slug', currentHost);
if (!user && !PUBLIC_ROUTES.some(r => request.nextUrl.pathname.startsWith(r))) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
return response;
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|api/webhooks).*)'],
};
Der x-tenant-slug Header ist reines Gold. Benutze ihn, um deine Server Components und API Routes wissen zu lassen, mit welchem Tenant sie es zu tun haben. Falls du mit uns an einem Next.js Projekt zusammenarbeitest, ist das Setzen das erste Ding unseres ersten Tages.
Authentication Flow in Multi-Tenant Apps
Supabase Auth bleibt neutral im Multi-Tenancy Spiel. User existieren in einer globalen Sphäre – Tenant-Relationen sind dein Rätsel zum Lösen. Hier ist unser Spielplan:
- User registriert sich: Erstelle einen Auth User, baue eine Organization, und erschaffe eine Membership mit einer 'owner'-Rolle.
- User wird eingeladen: Der Admin zeichnet eine ausstehende Einladung auf, ein neuer User tritt über den Einladungs-Link bei, und poof – eine Membership erscheint mit der angegebenen Rolle.
- User loggt sich ein: Extrahiere Tenant aus Subdomain, bestätige Membership, eskortiere sie zu ihrem Dashboard.
// app/api/auth/signup/route.ts
import { createClient } from '@/lib/supabase/server';
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const { email, password, orgName, orgSlug } = await request.json();
const supabase = await createClient();
// Registriere den User
const { data: authData, error: authError } = await supabase.auth.signUp({
email,
password,
});
if (authError) return NextResponse.json({ error: authError.message }, { status: 400 });
// Benutze einen Service Role Client für Org-Erstellung (überspringt RLS)
const adminClient = createAdminClient();
const { data: org, error: orgError } = await adminClient
.from('organizations')
.insert({ name: orgName, slug: orgSlug })
.select()
.single();
if (orgError) return NextResponse.json({ error: orgError.message }, { status: 400 });
// Erstelle Owner Membership
await adminClient
.from('memberships')
.insert({
user_id: authData.user!.id,
org_id: org.id,
role: 'owner',
});
return NextResponse.json({ org });
}
Bemerke, dass wir einen Service Role Client während der Signup benutzen. Der User hat noch keine Memberships, also würde RLS ihn hilflos bei der Organization-Erstellung lassen. Es ist eines dieser klassischen Bootstrap-Probleme – dein Service Role Key wird dein Zauberstab sein.
Und ich kann das nicht genug betonen: Exposiere deinen Service Role Key niemals gegenüber dem Client. Es ist strikt für Server-Side Code.
Server Components und RLS: Das SSR Problem
Next.js 15's Server Components sind Server-gebunden und erhöhen das Sicherheitsspiel. Aber es gibt einen Knick, wenn man Supabase RLS benutzt: du musst die Session des Users zum Supabase Client geben, damit RLS Policies wissen, wer am Tisch sitzt.
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
export async function createClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
} catch {
// Das kann in Server Components fehlschlagen (read-only)
// Middleware kümmert sich um Cookie-Erneuerung
}
},
},
}
);
}
// app/[orgSlug]/projects/page.tsx
import { createClient } from '@/lib/supabase/server';
import { headers } from 'next/headers';
export default async function ProjectsPage() {
const supabase = await createClient();
const headersList = await headers();
const tenantSlug = headersList.get('x-tenant-slug');
// Hole die Org ID aus dem Slug
const { data: org } = await supabase
.from('organizations')
.select('id')
.eq('slug', tenantSlug)
.single();
if (!org) return <div>Organization nicht gefunden</div>;
// RLS filtert automatisch – gibt nur Projekte zurück
// wobei der aktuelle User eine Membership hat
const { data: projects } = await supabase
.from('projects')
.select('*')
.eq('org_id', org.id)
.order('created_at', { ascending: false });
return (
<div>
{projects?.map(project => (
<ProjectCard key={project.id} project={project} />
))}
</div>
);
}
Hier ist der Kniff: Selbst wenn jemand die org_id in der Request fälscht, regt sich RLS nicht. Es blockiert den Zugang zu Projekten, es sei denn, der User ist Mitglied. Technisch ist .eq('org_id', org.id) redundant für Sicherheit – RLS kümmert sich darum – aber es ist gut für Performance und Lesbarkeit.
Performance-Optimierung und Common Pitfalls
Das N+1 RLS Query Problem
Jeder RLS Policy Check spinnt eine Subquery auf. Hook auf eine 10-row Policy Check, wenn du auf 100 Reihen schielst, bedeutet 100 Runden Membership Lookup. Glücklicherweise ist PostgreSQL smart genug zum Cachen – aber es gibt Overhead.
Fix: Benutze STABLE auf Helper Functions (wie wir skizzierten). Auch, denk daran, org_id in die JWT Claims zu denormalisieren:
-- Custom JWT Hook (Supabase Dashboard > Auth > Hooks)
CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event JSONB)
RETURNS JSONB AS $$
DECLARE
org_ids UUID[];
BEGIN
SELECT array_agg(org_id) INTO org_ids
FROM memberships
WHERE user_id = (event->>'user_id')::UUID;
event := jsonb_set(
event,
'{claims,org_ids}',
to_jsonb(org_ids)
);
RETURN event;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
Dann wird deine RLS Policy:
CREATE POLICY "Mitglieder können sehen"
ON projects FOR SELECT
USING (
org_id = ANY(
(SELECT array(SELECT jsonb_array_elements_text(
auth.jwt()->'org_ids'
))::UUID[])
)
);
Das schlägt das Membership Table Lookup völlig aus. Die Org IDs kommen direkt vom JWT. Caveat: JWT Claims sind beim Login gestempelt. Ändere jemandes Membership, und er muss sich neu authentifizieren, um Claims zu synchronisieren. Typischerweise ist das völlig bewältigbar – halte es einfach in deinen Docs.
Connection Pooling
Supabase schöpft Connection Pooling durch PgBouncer aus. Falls du mit Next.js auf Vercel live gehst, merke dir: Pooler URL für API Routes und Server Components.
# Für reguläre Operationen (gepooled)
DATABASE_URL=postgres://user:pass@db.project.supabase.co:6543/postgres
# Nur für Migrationen (direkt)
DIRECT_URL=postgres://user:pass@db.project.supabase.co:5432/postgres
Alle auf Supabase's Pro für $25 pro Monat bekommen 200 concurrent Connections via den Pooler. Für die meisten SaaS Apps schüchtern vor 1000 Benutzern gleichzeitig ist es mehr als genug.
Indexes, die du absolut brauchst
Hier ist der Brute-Force Index Set für ein Multi-Tenant Setup:
-- Auf jedem Tenant-Scoped Table
CREATE INDEX idx_{table}_org_id ON {table}(org_id);
-- Composite Indexes für häufige Queries
CREATE INDEX idx_projects_org_created ON projects(org_id, created_at DESC);
-- Memberships – schwer durchsucht von RLS
CREATE INDEX idx_memberships_user_org ON memberships(user_id, org_id);
CREATE INDEX idx_memberships_org_role ON memberships(org_id, role);
EXPLAIN ANALYZE – eines Developers bester Freund. Sieh, wie deine Queries mit RLS an Bord abschneiden. Du könntest einen rauhen Erwachen bekommen über das, was der Planner beschließt zu tun ohne die richtigen Indexes.
Testen von RLS Policies
Alle springen über dies hinweg, doch es ist dein bestes Sicherheitsnetz gegen Datenlecks. Wir testen RLS Policies direkt in SQL:
-- Test als spezifischer User
SET request.jwt.claims = '{"sub": "user-uuid-here", "role": "authenticated"}';
SET role = 'authenticated';
-- Das sollte nur Projekte zurückgeben, auf die der User Zugriff hat
SELECT * FROM projects;
-- Das sollte fehlschlagen (User ist nicht Mitglied dieser Org)
INSERT INTO projects (org_id, name) VALUES ('other-org-uuid', 'Sneaky Project');
-- Zurücksetzen
RESET role;
Und lass uns nicht pgTAP für kritische Policies vergessen:
BEGIN;
SELECT plan(3);
-- Richte Testkontext als User A auf (Mitglied von Org 1)
SET LOCAL request.jwt.claims = '{"sub": "user-a-uuid"}';
SET LOCAL role = 'authenticated';
SELECT is(
(SELECT count(*) FROM projects WHERE org_id = 'org-1-uuid')::INTEGER,
5,
'User A sieht 5 Projekte in ihrer Org'
);
SELECT is(
(SELECT count(*) FROM projects WHERE org_id = 'org-2-uuid')::INTEGER,
0,
'User A sieht 0 Projekte in anderer Org'
);
SELECT throws_ok(
$$INSERT INTO projects (org_id, name) VALUES ('org-2-uuid', 'Hack')$$,
'new row violates row-level security policy',
'User A kann nicht in anderer Org einfügen'
);
SELECT * FROM finish();
ROLLBACK;
Lauf diese in CI. Jede Migration, die mit RLS Policies spielt, sollte die volle Test Suite durch ein kräftiges Trainingsprogramm schicken.
Production Deployment Checkliste
Bereit zum Shippieren? Rüste dich mit diesem aus:
- RLS enabled auf jedem Table, der Tenant-Daten hält
- Service Role Key gehütet Server-seitig, nirgendwo in die Nähe eines Clients
-
org_idrichtig indexiert auf allen Tenant-Scoped Tables - Membership Helper Functions geadelted als
SECURITY DEFINERundSTABLE - JWT Custom Claims gesperrt und geladen (falls auf der JWT Route)
- Connection Pooling für Cloud Deployment gehakt?
- RLS Policies frisch aus QA Testing mit pgTAP oder ähnlichem
-
EXPLAIN ANALYZEauf entscheidenden Queries mit RLS laufend getestet - Invite/Signup Flow nicht mit Membership Bootstraps ausgelassen
- Rate Limiting auf Auth Endpoints? Supabase bietet baked-in Optionen
- RLS für
authSchema Tables in Supabase Dashboard umgeschaltet (oft eine Landmine) - Embedded Monitoring für langsame Queries eingerichtet (Supabase Dashboard > Database > Query Performance)
Ein Multi-Tenant Produkt launchen und willst jemanden, der diese Gewässer gewatet ist? Unsere Headless CMS Development Solutions oder ein Quick Chat über unsere Kontaktseite könnten genau sein, was du brauchst.
FAQ
Kann ich Supabase RLS für Apps mit Tausenden von Mandanten benutzen?
Absolut. Wir haben Shared-Table RLS mit 5.000+ Mandanten und Millionen Reihen gepilot, ohne einen Nerv zu brechen. Das Geheimnis? Richtige Indexierung auf org_id Spalten und STABLE Helper Functions. 50.000+ Mandanten oder Milliarden-Reihen Extravaganzas im Sinn? Tauche in Partitionierung von Tables durch org_id ein oder flirte mit einem Schema-pro-Mandant Setup.
Wie handhabe ich Tenant Switching, wenn ein User mehreren Organizations angehört?
Halt die aktive Organization in einem Cookie oder URL (Subdomain) verstaut. Tausche Orgs? Ändere Subdomain/Cookie und hole frisch. Tuck die aktive Org nicht in den JWT – es verlangt einen Relog zu ändern. Ein Cookie, das deine Middleware ausspielen kann, ist der Weg.
Was passiert, wenn ich vergesse, RLS auf einem Table zu enablen?
Jeder authentifizierte User könnte jeden Row antippen. Das ist PostgreSQL's Standardhaltung – keine Zeilenebene Beschränkungen auf Tables ohne RLS. Supabase Dashboard kennzeichnet Tables, denen RLS fehlt, aber das Einbetten in CI mit Queries zu pg_tables und pg_policies hilft auch.
Sollte ich Supabase's Service Role Key benutzen oder eine Custom PostgreSQL Role für Admin Aufgaben kochen?
Meistens tut der Service Role Key's. Es überspringt RLS ganz, also es ist dein Top Secret nur für Server-Side Verwendung. Brauchst du granulare Governance (wie eine "admin" Role, die in allen Orgs herumschleicht, aber scheu vor Löschungen)? Das ist Custom PostgreSQL Territorium – fortgeschritten und generell off deinem Radar bis komplexe interne Tools es fordern.
Wie fahre ich Datenbank-Migrationen durch, ohne über RLS Policies zu stolpern?
Supabase's CLI (supabase db push oder supabase migration) neben der direkten Datenbank URL (Pooling übergehend) hat deinen Rücken. Tuck RLS Policy Edits in die gleiche Migration wie Schema Tweaks. Test fahr Migrationen gegen ein Staging Projekt – Supabase lässt dich Preview Branches auf Pro für genau diese Sache spinnen.
Können RLS Policies auf Daten aus anderen APIs oder Services erreichen?
Nein. RLS Policies sitzen schnug in SQL, bewertet von PostgreSQL. Magst checking auf externe Daten (wie einen Feature Flag Service)? Zement die Daten in einen Database Table, dann referenziere in deiner Policy. Ein typisches Pattern ist Synchronisierung von Subscription Statussen von Stripe zu einer organizations.plan Spalte.
Was ist die Performance Tax von RLS im Vergleich zu Filterung auf Anwendungsebene?
Über in unseren Supabase Pro Benchmarks (2 vCPUs, 8GB RAM), schiebt RLS einen Extra 1-3ms pro Query für Basis Membership-Check Policies mit den richtigen Indexes. Geh wild mit Policy Komplexität oder Joins und du könntest 5-15ms hinzufügen. Die JWT Claims Taktik (Speichern von org_ids im Token) schneidet es unter 1ms, da es keinen Subquery Tanz gibt. Für typische Web Apps, ist das Rinnsal von Latenz vernachlässigbar.
Wie funktioniert das mit Supabase Realtime Subscriptions?
Supabase Realtime spielt nach dem RLS Regelwerk. Tune in zu Table Changes und fange nur Events von Reihen, auf die du Zugriff hast laut RLS. Das rollt out of the box mit null Extra Tinkering. Just sei sicher, dass dein Client-Side Supabase die User Session hat, welche @supabase/ssr nahtlos kümmert.