Warum Supabase RLS für Multi-Tenancy

Bleiben wir ehrlich: Wenn es um Multi-Tenancy in SaaS-Apps geht, hast du Optionen. Du kannst eine separate Datenbank pro Mandant einrichten, was wie organisatorisches Paradies klingt, aber teuer und verdammt schmerzhaft zu verwalten ist. Oder du versuchst es mit separaten Schemas, was operativ weniger Ärger bereitet, aber Migrationen sind immer noch kein Spaziergang. Aber dann gibt es noch den Liebling der SaaS-Welt—freigegebene Tabellen mit Zeilenfilterung. Supabase macht diesen Ansatz dank PostgreSQL-nativem Row Level Security (RLS) zum Kinderspiel.

Warum sollte dir das überhaupt wichtig sein? Einfach. Deine Datenfilterung erfolgt auf Datenbankebene. Wenn du in deiner WHERE-Klausel in deiner Next.js-API-Route Mist baust, brauchst du nachts nicht über Datenschutzverletzungen nachzudenken, weil die Datenbank selbst dein Sicherheitsnetz ist. Und ehrlich, heutzutage ist das keine Luxusausstattung—das ist eine Notwendigkeit.

Aber lassen wir uns nichts vormachen. RLS addiert Overhead zu deinen Abfragen, erschwert das Debugging und kann dir während Migrationen ein Bein stellen. Wie schneiden also verschiedene Multi-Tenancy-Ansätze ab?

Ansatz Isolationsstufe Kosten Operationale Komplexität Abfrageleistung
Datenbank pro Mandant Vollständig Hoch ($50-200/Mandant/Mo) Sehr hoch Beste
Schema pro Mandant Stark Mittel Hoch (Migrationen) Gut
Freigegebene Tabellen + RLS Zeilenebene Niedrig Mittel Gut (mit Vorbehalten)
Filterung auf Anwendungsebene Keine Niedrigste Niedrig Beste

Für die meisten SaaS-Produkte mit weniger als 10.000 Mandanten bieten freigegebene Tabellen mit RLS das beste Preis-Leistungs-Verhältnis. Das ist das Thema hier.

Multi-Tenant Next.js with Supabase RLS: Production Guide

Architekturmuster: Gemeinsam vs. Isoliert

Bevor du auch nur anfängst, Code zu schreiben, musst du deine Mandantenauflösungsstrategie wählen. In der Praxis stößt du meistens auf zwei Ansätze:

Subdomain-basierte Mandanz

Schon mal tenant-slug.yourapp.com gesehen? Willkommen zum häufigsten Muster für B2B-SaaS. Es ist elegant, professionell und macht die Mandantenauflösung in Middleware zu einem Kinderspiel.

Pfad-basierte Mandanz

Das hier ist dein Standard /org/tenant-slug/dashboard. Einfacher zu einzurichten, da es keine Wildcard-DNS gibt, und es funktioniert auf Plattformen wie Vercel ohne benutzerdefinierte Domains. Aber sei ehrlich: Es wirkt ein bisschen wie Socken zu Sandalen zu tragen. Wir empfehlen normalerweise Subdomains für produktive B2B-Apps und pfadbasiert für interne Tools oder MVPs. Später wechseln? Du wirst deinem früheren Ich verfluchen—diese Muster zu ändern ist keine leichte Angelegenheit.

Einrichten des Mandantenschemas

Hier ist ein Schemamuster, das uns in drei verschiedenen produktiven Rollouts nicht im Stich gelassen hat:

-- Kern-Mandantentabelle
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 '{}'
);

-- Mitgliedschafts-Junction-Tabelle
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-Mandantengebundene Tabelle
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 das auf JEDER mandantengebundenen Tabelle
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-Richtlinien werden darauf verweisen, als ob es ihr Lieblingscousin ist. Benutzer können mehreren Organisationen beitreten, und ihre Rollen diktieren, was sie können und was nicht. Und hier ist ein kleiner Nugget der Weisheit: Immer—ernsthaft, immer—org_id auf jeder mandantengebundenen Tabelle indizieren. Ansonsten sieh zu, wie deine Abfragen kriechend langsam werden, sobald du in Daten schwimmst. Wir wurden überrascht, als sich das Dashboard eines Kunden mit 100.000 Reihen von 50ms auf 8 Sekunden verschlechterte. Lektion gelernt.

RLS-Richtlinien, die wirklich skalieren

Hier ist, wo Tutorials normalerweise den Hut nehmen und dich hilflos zurücklassen. Sie werfen auth.uid() = user_id auf dich und sagen, „Viel Glück!" Aber Multi-Tenant-RLS lässt sich nicht so einfach herunterkochen.

Das Helper-Funktions-Muster

Warum jede Richtlinie mit Mitgliedschaftschecks zumüllen? Nutze stattdessen eine Helper-Funktion:

-- Helper: überprüfe, ob der aktuelle Benutzer 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 die Rolle des Benutzers 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 Erstellers ausgeführt wird und RLS auf der memberships-Tabelle überspringt. Ohne dies riskierst du, in ein zirkuläres Abhängigkeitsloch zu fallen, bei dem RLS auf memberships die Mitgliedschaftschecks abstürzt, auf die sich andere Tabellen verlassen.

Und der STABLE-Teil? Er signalisiert dem Query-Planner, dass die Ausgabe der Funktion während einer einzelnen Abfrage für dieselbe Eingabe konsistent bleibt, was einige schöne Caching-Vorteile ermöglicht. Versucht, IMMUTABLE zu verwenden? Nicht tun. Mitgliedschaften können sich zwischen Transaktionen ändern.

Richtlinien für mandantengebundene Tabellen

Schauen wir uns einige Richtlinien für unsere projects-Tabelle an:

-- Aktiviere RLS
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

-- SELECT: Mitglieder können Projekte in ihren Orgs einsehen
CREATE POLICY "Members can view org projects"
  ON projects FOR SELECT
  USING (public.is_member_of(org_id));

-- INSERT: Admins und Owner können Projekte erstellen
CREATE POLICY "Admins can create projects"
  ON projects FOR INSERT
  WITH CHECK (
    public.get_role_in(org_id) IN ('owner', 'admin')
  );

-- UPDATE: Admins und Owner können Projekte aktualisieren
CREATE POLICY "Admins can update projects"
  ON projects FOR UPDATE
  USING (public.is_member_of(org_id))
  WITH CHECK (
    public.get_role_in(org_id) IN ('owner', 'admin')
  );

-- DELETE: nur Owner können Projekte löschen
CREATE POLICY "Owners can delete projects"
  ON projects FOR DELETE
  USING (
    public.get_role_in(org_id) = 'owner'
  );

Richtlinien für die Memberships-Tabelle selbst

Diese hier ist knifflig. Die memberships-Tabelle bekommt ihre eigene RLS, kann aber die Helper-Funktionen nicht verwenden, weil diese wiederum memberships abfragen—ein Zirkelverweis-Albtraum:

ALTER TABLE memberships ENABLE ROW LEVEL SECURITY;

-- Benutzer können Mitgliedschaften in Orgs sehen, zu denen sie gehören
CREATE POLICY "Users can view org memberships"
  ON memberships FOR SELECT
  USING (
    org_id IN (
      SELECT org_id FROM memberships WHERE user_id = auth.uid()
    )
  );

-- Nur Owner können Mitglieder hinzufügen
CREATE POLICY "Owners can add members"
  ON memberships FOR INSERT
  WITH CHECK (
    org_id IN (
      SELECT org_id FROM memberships
      WHERE user_id = auth.uid() AND role = 'owner'
    )
  );

Ja, es gibt eine Unterabfrage auf der gleichen Tabelle. Und ja, PostgreSQL nailed it. Die Unterabfrage überprüft deine eigene Mitgliedschaft, unberührt von der Richtlinie, die definiert wird, da RLS nur die äußere Abfrage umhüllt. Aber teste das—wirklich, du willst keinen Bug in der Produktion finden.

Multi-Tenant Next.js with Supabase RLS: Production Guide - architecture

Next.js Middleware für Mandantenauflösung

Mit Next.js 15 und dem schicken App Router ist Middleware, die am Edge läuft, der perfekte Hausverwalter für Mandantenauflösung. Hier ist unser bewährtes Muster 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];

  // Überspinge für Hauptdomain 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. Nutze ihn, um deine Server Components und API-Routes wissen zu lassen, mit welchem Mandanten sie es zu tun haben. Wenn du mit uns an einem Next.js-Projekt zusammenarbeitest, ist das Setup unser erster Tag Priorität.

Authentifizierungsfluss in Multi-Tenant-Apps

Supabase Auth bleibt neutral im Multi-Tenancy-Spiel. Benutzer existieren in einer globalen Sphäre—Mandantenbeziehungen sind dein Puzzle. Hier ist unser Spielplan:

  1. Benutzer registriert sich: Erstelle einen Auth-Benutzer, baue eine Organisation auf und zaubere eine Mitgliedschaft mit einer „owner"-Rolle hervor.
  2. Benutzer wird eingeladen: Der Admin zeichnet eine ausstehende Einladung auf, ein neuer Benutzer tritt über den Einladungslink bei und—poof—eine Mitgliedschaft mit der angegebenen Rolle erscheint.
  3. Benutzer meldet sich an: Extrahiere Mandanten aus Subdomain, bestätige Mitgliedschaft, eskortiere ihn zu seinem 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 Benutzer
  const { data: authData, error: authError } = await supabase.auth.signUp({
    email,
    password,
  });

  if (authError) return NextResponse.json({ error: authError.message }, { status: 400 });

  // Nutze 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-Mitgliedschaft
  await adminClient
    .from('memberships')
    .insert({
      user_id: authData.user!.id,
      org_id: org.id,
      role: 'owner',
    });

  return NextResponse.json({ org });
}

Beachte, dass wir während der Registrierung auf einen Service-Role-Client verlassen. Der Benutzer hat noch keine Mitgliedschaften, also würde RLS ihn für die Organisationserstellung im Stich lassen. Es ist eines dieser klassischen Bootstrap-Probleme—dein Service-Role-Key wird dein Zauberstab sein.

Und ich kann das nicht genug betonen: Gib deinen Service-Role-Key niemals, niemals an den Client weiter. Das ist ausschließlich für Server-seitigen Code.

Server Components und RLS: Das SSR-Problem

Next.js 15s Server Components sind serverseitig gebunden und erhöhen das Sicherheitsspiel. Aber es gibt einen Haken bei der Verwendung von Supabase RLS: Du musst die Sitzung des Benutzers an den Supabase-Client übergeben, damit RLS-Richtlinien 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 (nur Lesen)
            // Die Middleware kümmert sich um Cookie-Refresh
          }
        },
      },
    }
  );
}
// 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 not found</div>;

  // RLS filtert automatisch — gibt nur Projekte zurück,
  // bei denen der aktuelle Benutzer Mitglied ist
  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 Knackpunkt: Selbst wenn jemand die org_id in der Anfrage manipuliert, RLS rührt sich nicht. Es blockiert den Zugriff auf Projekte, wenn der Benutzer kein Mitglied ist. Technisch gesehen ist .eq('org_id', org.id) für Sicherheit redundant—RLS handhabt das—aber es ist gut für Leistung und Lesbarkeit.

Leistungsoptimierung und häufige Fallstricke

Das N+1 RLS-Abfrage-Problem

Jede RLS-Richtlinienüberprüfung dreht eine Unterabfrage. Wenn du auf eine 10-Zeilen-Richtlinienüberprüfung tappst, während du 100 Zeilen betrachtest, bedeutet das 100 Runden Mitgliedschaftssuche. Glücklicherweise ist PostgreSQL klug genug zum Zwischenspeichern—aber es gibt Overhead.

Fix: Nutze STABLE auf Helper-Funktionen (wie wir aufgelistet haben). Auch, denke darüber nach, 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-Richtlinie zu:

CREATE POLICY "Members can view"
  ON projects FOR SELECT
  USING (
    org_id = ANY(
      (SELECT array(SELECT jsonb_array_elements_text(
        auth.jwt()->'org_ids'
      ))::UUID[])
    )
  );

Das beseitigt die Mitgliedschaftstabellensuche völlig. Die Org-IDs kommen direkt aus dem JWT. Caveat: JWT-Claims werden beim Login gestempelt. Ändere jemandes Mitgliedschaft, und er muss sich neu authentifizieren, um Claims zu synchronisieren. Normalerweise ist das völlig handhabbar—halte es nur in deinen Docs fest.

Connection Pooling

Supabase stellt Connection Pooling über PgBouncer bereit. Wenn du mit Next.js auf Vercel live gehst, denke daran: Pooler-URL für API-Routes und Server Components.

# Für normale 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

Jeder auf Supabase's Pro für $25 pro Monat bekommt 200 gleichzeitige Verbindungen über den Pooler. Für die meisten SaaS-Apps mit weniger als 1000 gleichzeitigen Benutzern ist das mehr als genug.

Indizes, die du absolut brauchst

Hier ist das Brute-Force-Indexset für ein Multi-Tenant-Setup:

-- Auf jeder mandantengebundenen Tabelle
CREATE INDEX idx_{table}_org_id ON {table}(org_id);

-- Zusammengesetzte Indizes für häufige Abfragen
CREATE INDEX idx_projects_org_created ON projects(org_id, created_at DESC);

-- Memberships — häufig von RLS abgefragt
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—ein Entwicklerbester Freund. Sieh dir an, wie deine Abfragen mit RLS abschneiden. Du könntest eine unangenehme Überraschung bekommen, was der Planner ohne die richtigen Indizes beschließt.

RLS-Richtlinien testen

Jeder überspringt das, doch es ist dein bestes Sicherheitsnetz gegen Datenlecks. Wir testen RLS-Richtlinien direkt in SQL:

-- Teste als ein spezifischer Benutzer
SET request.jwt.claims = '{"sub": "user-uuid-here", "role": "authenticated"}';
SET role = 'authenticated';

-- Das sollte nur Projekte zurückgeben, auf die der Benutzer Zugriff hat
SELECT * FROM projects;

-- Das sollte fehlschlagen (Benutzer ist nicht Mitglied dieser Org)
INSERT INTO projects (org_id, name) VALUES ('other-org-uuid', 'Sneaky Project');

-- Zurücksetzen
RESET role;

Und vergiss pgTAP für kritische Richtlinien nicht:

BEGIN;
SELECT plan(3);

-- Richte Test-Kontext als Benutzer 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 sees 5 projects in their org'
);

SELECT is(
  (SELECT count(*) FROM projects WHERE org_id = 'org-2-uuid')::INTEGER,
  0,
  'User A sees 0 projects in other org'
);

SELECT throws_ok(
  $$INSERT INTO projects (org_id, name) VALUES ('org-2-uuid', 'Hack')$$,
  'new row violates row-level security policy',
  'User A cannot insert into other org'
);

SELECT * FROM finish();
ROLLBACK;

Führe diese in CI aus. Jede Migration, die mit RLS-Richtlinien spielt, sollte die vollständige Test-Suite durch ein intensives Workout schicken.

Production-Deployment-Checkliste

Bereit zu versenden? Rüste dich mit diesem aus:

  • RLS aktiviert auf jeder Tabelle, die Mandantendaten enthält
  • Service-Role-Key gehütet auf der Serverseite, nirgendwo in der Nähe eines Clients
  • org_id ordnungsgemäß auf allen mandantengebundenen Tabellen indiziert
  • Mitgliedschafts-Helper-Funktionen als SECURITY DEFINER und STABLE geadelt
  • JWT benutzerdefinierte Claims verriegelt und geladen (falls auf der JWT-Route)
  • Connection Pooling für Cloud-Deployment eingehakt?
  • RLS-Richtlinien frisch aus QA-Tests mit pgTAP oder ähnlich
  • EXPLAIN ANALYZE auf entscheidende Abfragen mit RLS durchgeführt
  • Invite-/Signup-Flow vermisst keine Mitgliedschafts-Bootstraps
  • Rate Limiting auf Auth-Endpoints? Supabase bietet eingebaute Optionen
  • Den Schalter für RLS auf auth-Schema-Tabellen im Supabase Dashboard umgelegt (oft eine Landmine)
  • Überwachung für langsame Abfragen aktiviert (Supabase Dashboard > Database > Query Performance)

Startest du ein Multi-Tenant-Produkt und wünschst dir jemanden, der diese Gewässer bereits durchquert hat? Unsere Headless-CMS-Entwicklungslösungen oder ein schneller Chat über unsere Kontaktseite könnten genau das sein, was du brauchst.

FAQ

Kann ich Supabase RLS für Apps mit Tausenden von Mandanten verwenden?

Absolut. Wir haben freigegebene-Tabelle RLS mit 5.000+ Mandanten und Millionen von Reihen getestet, ohne zu schwitzen. Die geheime Zutat? Richtige Indizierung auf org_id-Spalten und STABLE-Helper-Funktionen. Denkst du an 50.000+ Mandanten oder Milliarden-Zeilen Abenteuer? Tauche in die Tabellen-Partitionierung nach org_id ein oder flirte mit einem Schema-pro-Mandant-Setup.

Wie handle ich Mandantenwechsel, wenn ein Benutzer mehreren Organisationen angehört?

Halte die aktive Organisation in einem Cookie oder URL (Subdomain). Wechsle Orgs? Ändere die Subdomain/das Cookie und hole neu ab. Lege die aktive Org nicht in das JWT—es erfordert eine Neuanmeldung zum Wechseln. Ein Cookie, das deine Middleware einsehen kann, ist der Weg dorthin.

Was passiert, wenn ich vergesse, RLS auf einer Tabelle zu aktivieren?

Jeder authentifizierte Benutzer könnte auf jede Zeile zugreifen. Das ist PostgreSQLs Standardposition—keine Zeilenbeschränkungen auf Tabellen ohne RLS. Supabase Dashboard markiert Tabellen, die RLS vermissen, aber das Einbetten in CI mit Abfragen zu pg_tables und pg_policies hilft auch.

Sollte ich Supabase's Service-Role-Key verwenden oder eine benutzerdefinierte PostgreSQL-Rolle für Admin-Aufgaben kochen?

Größtenteils reicht der Service-Role-Key aus. Er umgeht RLS völlig, also ist er dein Top-Secret nur für Server-seitige Nutzung. Brauchst du granulare Governance (wie eine „admin"-Rolle in allen Orgs, aber scheu vor Löschungen)? Das ist benutzerdefiniertes PostgreSQL-Territorium—fortgeschritten und normalerweise außerhalb deines Radars, bis komplexe interne Tools es verlangen.

Wie führe ich Datenbankmigrationen aus, ohne über RLS-Richtlinien zu stolpern?

Supabase's CLI (supabase db push oder supabase migration) zusammen mit der direkten Datenbank-URL (Pooler übergehend) hat deinen Rücken. Packe RLS-Richtlinienbearbeitungen in die gleiche Migration wie Schema-Änderungen. Gießtest du Migrationen gegen ein Staging-Projekt—Supabase lässt dich Preview-Branches auf Pro genau dafür hochfahren.

Können RLS-Richtlinien Daten von anderen APIs oder Diensten erreichen?

Nope. RLS-Richtlinien sitzen gemütlich in SQL, bewertet von PostgreSQL. Fancy-Überprüfung auf externe Daten (wie ein Feature-Flag-Service)? Zementiere diese Daten in einer Datenbanktabelle, dann verweise in deiner Richtlinie. Ein typisches Muster ist das Synchronisieren von Abonnementstatus von Stripe zu einer organizations.plan-Spalte.

Was ist die Leistungssteuer von RLS im Vergleich zum Filtern auf Anwendungsebene?

Über in unseren Supabase Pro-Benchmarks (2 vCPUs, 8GB RAM) addiert RLS extra 1-3ms pro Abfrage für grundlegende Mitgliedschafts-Check-Richtlinien mit den richtigen Indizes. Wird wild mit Richtlinien-Komplexität oder Joins und du könntest 5-15ms addieren. Die JWT-Claims-Taktik (org_ids im Token speichern) schneidet es unter 1ms, da es keinen Unterabfrage-Tanz gibt. Für typische Web-Apps ist das Rinnsal an Latenz vernachlässigbar.

Wie funktioniert das mit Supabase Realtime-Abonnements?

Supabase Realtime spielte nach den RLS-Regeln. Tune in zu Tabellenänderungen und fangen nur Ereignisse von Zeilen, die du laut RLS berechtigt bist zu sehen. Dies rollte vorkonfiguriert ohne zusätzliche Tüftelei aus. Stelle einfach sicher, dass dein Client-seitiges Supabase die Benutzersitzung hat, die @supabase/ssr nahtlos handhabt.