Supabase RLS Multi-Tenant Production-Schema-Designleitfaden
Ich habe drei Multi-Tenant-SaaS-Apps auf Supabase in den letzten zwei Jahren deployed. Die erste war ein Desaster. Nicht weil Supabase mich im Stich gelassen hat -- das tat es nicht -- sondern weil ich grundlegend missverstanden habe, wie Row Level Security (RLS) mit Query Planning in großem Maßstab interagiert. Die zweite war besser. Die dritte verwaltet 2.000+ Mandanten mit Abfragezeiten unter 50ms über Tabellen mit Millionen von Zeilen.
Dieser Artikel ist alles, das ich mir vor dem ersten Projekt hätte sagen lassen sollen. Wir werden ein echtes Multi-Tenant-Schema von Grund auf aufbauen, RLS-Richtlinien einrichten, die tatsächlich performant sind, und die Edge Cases abdecken, die nur auftauchen, wenn echter Traffic deine Datenbank trifft.
Inhaltsverzeichnis
- Multi-Tenancy-Ansätze in Supabase
- Die Grundlage: Mandanten- und Benutzer-Schema
- RLS-Richtlinien entwerfen, die nicht die Performance zerstören
- Das tenant_id-Muster: Es richtig machen
- JWT-Claims vs. Datenbankabfragen
- Mandantenübergreifende Operationen handhaben
- Migrationsstrategie und Schema-Evolution
- Performance-Benchmarks und Optimierung
- Sicherheitsverhärtung für die Produktion
- Echtes Schema-Beispiel
- FAQ

Multi-Tenancy-Ansätze in Supabase
Bevor wir eine einzige Zeile SQL schreiben, lass uns die drei Ansätze zu Multi-Tenancy klären und warum einer davon für die meisten Supabase-Projekte gewinnt.
| Ansatz | Isolationsstufe | Komplexität | Kosten pro Mandant | Am besten für |
|---|---|---|---|---|
| Datenbank pro Mandant | Höchste | Sehr hoch | $25+/Mo pro Mandant | Enterprise, Compliance-intensiv |
| Schema pro Mandant | Hoch | Hoch | $5-15/Mo pro Mandant | Mid-Market SaaS |
| Gemeinsames Schema + RLS | Mittel | Mittel | Cent-Beträge pro Mandant | Die meisten SaaS-Apps |
Datenbank-pro-Mandant ist das, was du verwenden würdest, wenn du an Banken oder Gesundheitsfirmen verkaufst, die buchstäblich separate Infrastruktur benötigen. Supabase macht das nicht einfach -- du würdest mehrere Supabase-Projekte verwalten.
Schema-pro-Mandant (mit PostgreSQL-Schemas wie tenant_123.projects) klingt verlockend, wird aber zu einem Wartungsalb. Jede Migration läuft gegen jedes Schema. Ich habe das einmal versucht. Mit 400 Mandanten dauerte eine einfache ALTER TABLE-Migration 45 Minuten.
Gemeinsames Schema mit RLS ist der Sweet Spot für 90% der SaaS-Anwendungen. Ein Satz Tabellen, ein Satz Migrationen, und PostgreSQL's RLS-Richtlinien handhaben die Isolation. Das bauen wir hier.
Die Grundlage: Mandanten- und Benutzer-Schema
Beginnen wir mit den Core-Tabellen. Ich zeige dir das Schema, das ich in der Produktion verwende, kein Tutorial-Spielzeug.
-- UUID-Generierung aktivieren
create extension if not exists "uuid-ossp";
-- Tenants (Organisationen, Workspaces, wie auch immer du sie nennst)
create table public.tenants (
id uuid primary key default uuid_generate_v4(),
name text not null,
slug text unique not null,
plan text not null default 'free' check (plan in ('free', 'pro', 'enterprise')),
settings jsonb not null default '{}',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
-- Membership-Verknüpfungstabelle: verbindet auth.users mit Mandanten
create table public.tenant_memberships (
id uuid primary key default uuid_generate_v4(),
tenant_id uuid not null references public.tenants(id) on delete cascade,
user_id uuid not null references auth.users(id) on delete cascade,
role text not null default 'member' check (role in ('owner', 'admin', 'member', 'viewer')),
created_at timestamptz not null default now(),
unique(tenant_id, user_id)
);
-- Kritisch: Indizes, auf die sich RLS-Richtlinien verlassen
create index idx_tenant_memberships_user_id on public.tenant_memberships(user_id);
create index idx_tenant_memberships_tenant_id on public.tenant_memberships(tenant_id);
create index idx_tenant_memberships_user_tenant on public.tenant_memberships(user_id, tenant_id);
Zwei Dinge zu beachten. Erstens verwende ich eine Verknüpfungstabelle (tenant_memberships), anstatt eine tenant_id direkt im Benutzerprofil zu haben. Benutzer können mehreren Mandanten angehören -- das ist eine echte Anforderung für fast jede SaaS-App. Zweitens sind diese Indizes nicht optional. Ohne sie führt jede RLS-Überprüfung einen Sequential Scan der Memberships-Tabelle durch. Ich habe gesehen, dass dies einmal 200ms+ zu Abfragen hinzufügt, wenn du ein paar tausend Memberships hast.
RLS-Richtlinien entwerfen, die nicht die Performance zerstören
Hier ist, wo die meisten Tutorials dir versagen. Sie zeigen dir eine einfache Richtlinie wie:
-- NICHT IN DER PRODUKTION MACHEN
create policy "Benutzer können die Daten ihres Mandanten ansehen"
on public.projects for select
using (
tenant_id in (
select tenant_id from public.tenant_memberships
where user_id = auth.uid()
)
);
Das funktioniert. Es besteht deine Tests. Und dann deployest du es, bekommst 500 Benutzer, und fragst dich, warum dein Dashboard 4 Sekunden zum Laden braucht.
Das Problem ist, dass PostgreSQL diese Unterabfrage für jede einzelne Zeile evaluiert. Der Query Planner kann das manchmal optimieren, aber das tut er oft nicht -- besonders mit JOINs im Spiel.
Das Security Definer Function Pattern
Hier ist, was tatsächlich in der Produktion funktioniert:
-- Erstelle eine Funktion, die die Mandanten-IDs des Benutzers zurückgibt
-- SECURITY DEFINER bedeutet, dass sie mit den Berechtigungen des Funktionserstelllers läuft
-- Das ist kritisch: es umgeht RLS auf der Memberships-Tabelle selbst
create or replace function public.get_user_tenant_ids()
returns setof uuid
language sql
security definer
stable
set search_path = public
as $$
select tenant_id
from public.tenant_memberships
where user_id = auth.uid();
$$;
-- Nutze sie jetzt in Richtlinien
create policy "tenant_isolation_select"
on public.projects for select
using (tenant_id in (select public.get_user_tenant_ids()));
create policy "tenant_isolation_insert"
on public.projects for insert
with check (tenant_id in (select public.get_user_tenant_ids()));
create policy "tenant_isolation_update"
on public.projects for update
using (tenant_id in (select public.get_user_tenant_ids()))
with check (tenant_id in (select public.get_user_tenant_ids()));
create policy "tenant_isolation_delete"
on public.projects for delete
using (tenant_id in (select public.get_user_tenant_ids()));
Warum ist das schneller? Das STABLE-Schlüsselwort teilt PostgreSQL mit, dass die Funktion innerhalb einer einzelnen Anweisung das gleiche Ergebnis zurückgibt. Der Planner kann sie einmal aufrufen und das Ergebnis wiederverwenden. In meinen Benchmarks reduziert dies den RLS-Overhead um 60-80% bei Abfragen, die mehrere Zeilen berühren.
Die search_path-Falle
Siehst du das set search_path = public auf der Funktion? Das ist nicht optional. Ohne es könnte ein böswilliger Benutzer möglicherweise eine Funktion in einem anderen Schema erstellen, die auth.uid() überschattet und deine Sicherheit umgeht. Das Supabase-Team hat darüber geschrieben, aber es ist leicht zu übersehen.

Das tenant_id-Muster: Es richtig machen
Jede Tabelle, die mandantenspezifische Daten enthält, benötigt eine tenant_id-Spalte. Keine Ausnahmen. Auch wenn die Tabelle einen Foreign Key zu einer anderen mandantengebundenen Tabelle hat. Hier ist warum:
-- Deine Projekte-Tabelle
create table public.projects (
id uuid primary key default uuid_generate_v4(),
tenant_id uuid not null references public.tenants(id) on delete cascade,
name text not null,
created_at timestamptz not null default now()
);
-- Aufgaben gehören zu Projekten. Du könntest denken, tenant_id ist hier redundant.
create table public.tasks (
id uuid primary key default uuid_generate_v4(),
tenant_id uuid not null references public.tenants(id) on delete cascade,
project_id uuid not null references public.projects(id) on delete cascade,
title text not null,
status text not null default 'todo',
created_at timestamptz not null default now()
);
-- Index für RLS + häufige Abfragemuster
create index idx_projects_tenant on public.projects(tenant_id);
create index idx_tasks_tenant on public.tasks(tenant_id);
create index idx_tasks_project on public.tasks(project_id);
create index idx_tasks_tenant_project on public.tasks(tenant_id, project_id);
Ja, tenant_id auf tasks ist denormalisiert. Ja, es ist die richtige Wahl. Ohne es müsste die RLS-Richtlinie auf tasks zu projects joinen, um den Mandanten zu verifying -- und dieser JOIN passiert bei jeder Abfrage. Mit der denormalisierten tenant_id ist die RLS-Überprüfung ein einfaches Index-Lookup.
Ich erzwinge Konsistenz mit einem Trigger:
create or replace function public.verify_tenant_consistency()
returns trigger
language plpgsql
as $$
begin
if NEW.tenant_id != (
select tenant_id from public.projects where id = NEW.project_id
) then
raise exception 'tenant_id mismatch: task tenant does not match project tenant';
end if;
return NEW;
end;
$$;
create trigger check_task_tenant
before insert or update on public.tasks
for each row execute function public.verify_tenant_consistency();
JWT-Claims vs. Datenbankabfragen
Supabase lässt dich Custom Claims in das JWT-Token einbetten. Manche Leute verwenden dies, um die aktuelle Mandanten-ID zu speichern:
-- Aus JWT lesen (schnell, aber hat Einschränkungen)
auth.jwt() -> 'app_metadata' ->> 'current_tenant_id'
Versus es jedes Mal aus der Datenbank nachschlagen:
-- Datenbankabfrage (langsamer, aber immer aktuell)
select tenant_id from tenant_memberships where user_id = auth.uid()
| Aspekt | JWT-Claims | Datenbankabfrage |
|---|---|---|
| Geschwindigkeit | ~0.1ms | ~1-5ms |
| Aktualität | Veraltet bis Token-Refresh | Immer aktuell |
| Multi-Tenant-Umschaltung | Erfordert Token-Refresh | Unmittelbar |
| Sicherheit bei Sperrung | Verzögerung bis JWT abläuft | Unmittelbar |
| Implementierungskomplexität | Höher (benötigt Edge Function) | Niedriger |
Meine Empfehlung: Verwende Datenbankabfragen mit dem Security Definer Function Pattern für die meisten Apps. Der Performance-Unterschied ist vernachlässigbar, wenn du ordnungsgemäße Indizes hast, und du vermeidest eine ganze Klasse von Bugs rund um veraltete Tokens.
Wenn du 10.000+ gleichzeitige Benutzer bedienst und das Sparen von Millisekunden wichtig ist, dann ja, verschiebe die aktive Mandanten-ID in das JWT. Du wirst eine Supabase Edge Function (oder einen Hook) benötigen, um den Claim zu setzen, wenn Benutzer Mandanten wechseln, und du wirst den Token-Refresh-Flow auf dem Client handhaben müssen.
Mandantenübergreifende Operationen handhaben
Einige Operationen müssen legitim Mandantengrenzen überschreiten. Admin-Dashboards, Abrechnungssysteme, Analytics-Aggregation. Hier ist, wie man das sicher macht.
Service Role Key (Sparsam verwenden)
Der Supabase Service Role Key umgeht RLS vollständig. Verwende ihn nur in Server-seitigem Code -- exponiere ihn niemals dem Client.
// Nur Server-seitig (Next.js API Route, Edge Function, etc.)
import { createClient } from '@supabase/supabase-js';
const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY! // NIEMALS exponieren
);
// Das umgeht RLS - verwende mit äußerster Vorsicht
const { data } = await supabaseAdmin
.from('tenants')
.select('id, name, plan')
.eq('plan', 'enterprise');
Wenn du auf Next.js aufbaust (wir tun viel von diesem Zeug bei Social Animal), sind deine API Routes und Server Components der richtige Platz für Service Role-Operationen.
Datenbankfunktionen für kontrollierten mandantenübergreifenden Zugriff
Für präzisere Kontrolle erstelle spezifische Funktionen:
create or replace function public.admin_get_tenant_stats(target_tenant_id uuid)
returns json
language plpgsql
security definer
set search_path = public
as $$
declare
result json;
caller_role text;
begin
-- Verifiziere, dass der Aufrufer ein Admin des Ziel-Mandanten ist
select role into caller_role
from tenant_memberships
where user_id = auth.uid() and tenant_id = target_tenant_id;
if caller_role not in ('owner', 'admin') then
raise exception 'Unauthorized: requires admin role';
end if;
select json_build_object(
'project_count', (select count(*) from projects where tenant_id = target_tenant_id),
'task_count', (select count(*) from tasks where tenant_id = target_tenant_id),
'member_count', (select count(*) from tenant_memberships where tenant_id = target_tenant_id)
) into result;
return result;
end;
$$;
Migrationsstrategie und Schema-Evolution
tenant_id zu bestehenden Tabellen hinzuzufügen ist die Migration, die alle fürchten. Hier ist der Ansatz, der Downtime minimiert:
-- Schritt 1: Nullbare Spalte hinzufügen
alter table public.some_existing_table
add column tenant_id uuid references public.tenants(id);
-- Schritt 2: Backfill (tue dies in Batches für große Tabellen)
update public.some_existing_table set tenant_id = (
select tenant_id from public.projects
where projects.id = some_existing_table.project_id
)
where tenant_id is null;
-- Schritt 3: Nicht-Null-Constraint hinzufügen
alter table public.some_existing_table
alter column tenant_id set not null;
-- Schritt 4: Index hinzufügen
create index concurrently idx_some_table_tenant
on public.some_existing_table(tenant_id);
-- Schritt 5: RLS aktivieren und Richtlinien hinzufügen
alter table public.some_existing_table enable row level security;
create policy "tenant_isolation" on public.some_existing_table
for all using (tenant_id in (select public.get_user_tenant_ids()));
Das concurrently-Schlüsselwort bei der Index-Erstellung ist entscheidend -- ohne es wirst du die Tabelle für den gesamten Index-Build sperren. Auf einer Tabelle mit einer Million Zeilen könnte das Minuten Downtime sein.
Performance-Benchmarks und Optimierung
Ich führte Benchmarks auf einem Supabase Pro Plan ($25/Monat, ab Anfang 2025) mit 500.000 Zeilen in der tasks-Tabelle, verteilt auf 1.000 Mandanten, durch.
| Abfragemuster | Ohne RLS | Naive RLS | Optimierte RLS | Overhead |
|---|---|---|---|---|
| 50 Zeilen auswählen (einzelner Mandant) | 2.1ms | 18.4ms | 3.8ms | +81% |
| Einzelne Zeile einfügen | 1.2ms | 4.1ms | 1.9ms | +58% |
| Zählen mit Filter | 3.4ms | 22.1ms | 5.2ms | +53% |
| Über 2 Tabellen joinen | 4.8ms | 45.2ms | 8.1ms | +69% |
"Optimierte RLS" bedeutet: Security Definer Funktionen, ordnungsgemäße composite Indizes, denormalisierte tenant_id auf untergeordneten Tabellen, und STABLE Funktions-Volatilität.
Der naive Ansatz (Inline-Unterabfragen, fehlende Indizes) macht RLS langsam wirken. Optimiert, ist der Overhead völlig akzeptabel für Produktions-Workloads.
Zusätzliche Optimierungstipps
- Verwende composite Indizes mit
tenant_idals führender Spalte:create index on tasks(tenant_id, status, created_at) - Partitioniere große Tabellen nach
tenant_id, wenn ein einzelner Mandant Millionen von Zeilen hat - Verwende
pg_stat_statementsum langsame Abfragen zu finden -- Supabase exponiert das im Dashboard unter Database > Query Performance - Erwäge materialisierte Views für mandantenübergreifende Analytics anstatt teure Aggregationen in Echtzeit auszuführen
Sicherheitsverhärtung für die Produktion
RLS ist deine primäre Verteidigung, sollte aber nicht deine einzige sein.
1. Standard-Deny
Aktiviere RLS immer ohne Standard-Richtlinie -- das bedeutet, wenn du vergisst, eine Richtlinie zu einer neuen Tabelle hinzuzufügen, ist sie standardmäßig gesperrt, anstatt weit offen zu sein.
alter table public.new_table enable row level security;
-- Füge keine permissive Richtlinie hinzu, bis du die Zugriffsmuster durchdacht hast
2. Audit-Logging
create table public.audit_log (
id bigint generated always as identity primary key,
tenant_id uuid not null,
user_id uuid,
action text not null,
table_name text not null,
record_id uuid,
old_data jsonb,
new_data jsonb,
created_at timestamptz not null default now()
);
-- Generischer Audit-Trigger
create or replace function public.audit_trigger_func()
returns trigger
language plpgsql
security definer
set search_path = public
as $$
begin
if TG_OP = 'DELETE' then
insert into audit_log(tenant_id, user_id, action, table_name, record_id, old_data)
values (OLD.tenant_id, auth.uid(), TG_OP, TG_TABLE_NAME, OLD.id, to_jsonb(OLD));
return OLD;
else
insert into audit_log(tenant_id, user_id, action, table_name, record_id, new_data, old_data)
values (
NEW.tenant_id, auth.uid(), TG_OP, TG_TABLE_NAME, NEW.id,
to_jsonb(NEW),
case when TG_OP = 'UPDATE' then to_jsonb(OLD) else null end
);
return NEW;
end if;
end;
$$;
3. Rate Limiting am Edge
RLS verhindert Daten-Lecks, aber es verhindert nicht Missbrauch. Verwende Supabase Edge Functions oder dein Next.js Middleware für Rate Limiting. Wenn du mit Astro aufbaust, kannst du dies in deinen Server-Endpoints handhaben.
4. Teste deine Richtlinien
Schreibe echte Tests. Verwende Supabase's lokale Entwicklungsumgebung (supabase start) und teste, dass:
- Benutzer A die Daten des Mandanten von Benutzer B nicht sehen kann
- Das Entfernen einer Membership den Zugriff sofort widerruft
- Service Role RLS korrekt umgeht
- Insert/Update-Richtlinien Mandanten-ID-Manipulation verhindern
// Beispieltest mit Vitest
import { describe, it, expect } from 'vitest';
import { createClient } from '@supabase/supabase-js';
describe('RLS Policies', () => {
it('should prevent cross-tenant data access', async () => {
const userA = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
global: { headers: { Authorization: `Bearer ${tokenA}` } }
});
const { data, error } = await userA
.from('projects')
.select('*')
.eq('tenant_id', TENANT_B_ID);
expect(data).toHaveLength(0); // RLS sollte diese filtern
});
});
Echtes Schema-Beispiel
Hier ist ein kondensiertes, aber komplettes Schema für eine Projekt-Management-SaaS. Das ist nah dran an dem, das ich in der Produktion deployed habe.
-- RLS auf allen Tabellen aktivieren
alter table public.tenants enable row level security;
alter table public.tenant_memberships enable row level security;
alter table public.projects enable row level security;
alter table public.tasks enable row level security;
-- Tenants: Benutzer können Mandanten sehen, denen sie angehören
create policy "view_own_tenants" on public.tenants for select
using (id in (select public.get_user_tenant_ids()));
-- Nur Besitzer können Mandanten-Einstellungen aktualisieren
create policy "owners_update_tenants" on public.tenants for update
using (
id in (
select tenant_id from public.tenant_memberships
where user_id = auth.uid() and role = 'owner'
)
);
-- Memberships: Mitglieder können andere Mitglieder in ihren Mandanten sehen
create policy "view_tenant_members" on public.tenant_memberships for select
using (tenant_id in (select public.get_user_tenant_ids()));
-- Nur Admins+ können Memberships verwalten
create policy "admins_manage_members" on public.tenant_memberships for insert
with check (
tenant_id in (
select tenant_id from public.tenant_memberships
where user_id = auth.uid() and role in ('owner', 'admin')
)
);
-- Projects & Tasks: standard Mandanten-Isolation
create policy "tenant_projects" on public.projects for all
using (tenant_id in (select public.get_user_tenant_ids()))
with check (tenant_id in (select public.get_user_tenant_ids()));
create policy "tenant_tasks" on public.tasks for all
using (tenant_id in (select public.get_user_tenant_ids()))
with check (tenant_id in (select public.get_user_tenant_ids()));
Wenn du etwas wie dies aufbaust und ein Team möchtest, das das schon gemacht hat, schau dir unsere Headless CMS-Entwicklung an -- wir haben Supabase-Backends mit Headless-Frontends für mehrere Clients gekoppelt.
FAQ
Fügt Supabase RLS signifikante Latenz zu Abfragen hinzu?
Mit optimierten Richtlinien (Security Definer Funktionen, ordnungsgemäße Indizes, denormalisierte tenant_id) ist der Overhead typischerweise 50-80% oben auf die reine Abfragezeit. Für eine Abfrage, die ohne RLS 3ms dauert, erwartet 5-6ms damit. Das ist völlig akzeptabel für Produktionseinsatz. Der naive Ansatz kann 10-20x Overhead hinzufügen, das ist warum Optimierung so wichtig ist.
Kann ich RLS mit Supabase Realtime-Subscriptions verwenden?
Ja. Supabase Realtime respektiert RLS-Richtlinien. Wenn sich ein Client auf Änderungen in einer Tabelle abonniert, werden sie nur Events für Zeilen erhalten, die sie sehen dürfen. Das funktioniert automatisch -- du brauchst keine zusätzliche Filter-Logik auf dem Client. Stelle nur sicher, dass deine RLS-Richtlinien effizient sind, da sie bei jedem Broadcast evaluiert werden.
Wie handhabe ich Mandanten-Umschaltung in der UI?
Speichere die aktive Mandanten-ID in deinem App-Status (React Context, Zustand Store, etc.) und übergib sie als Filter in deinen Abfragen. Da RLS bereits Ergebnisse auf Mandanten limitiert, zu denen der Benutzer gehört, ist Umschaltung nur eine Sache der Änderung, welche tenant_id du filterst. Kein Token-Refresh nötig, wenn du den Datenbankabfrage-Ansatz verwendest.
Sollte ich Supabase's eingebaute Auth oder einen externen Provider wie Clerk verwenden?
Supabase Auth integriert sich natürlich mit RLS durch auth.uid(). Wenn du einen externen Provider verwendest, musst du Benutzer in Supabase synchronisieren und Custom JWT-Claims oder eine Mapping-Tabelle verwenden. Ich würde bei Supabase Auth bleiben, es sei denn, du hast einen spezifischen Grund, das nicht zu tun -- es spart erhebliche Integrations-Arbeit.
Wie viele Mandanten kann ein einzelnes Supabase-Projekt handhaben?
Mit gemeinsamer Schema Multi-Tenancy habe ich persönlich 2.000+ Mandanten auf einem Supabase Pro Plan ($25/Monat) betrieben. Die praktische Grenze hängt von total Datenvolumen und Abfragemustern ab, nicht von Mandanten-Zahl. Eine Supabase Pro-Instanz mit 2 vCPUs und 1GB RAM kann Tabellen mit Dutzenden Millionen Zeilen handhaben, wenn deine Indizes richtig sind.
Was passiert, wenn ich vergesse, RLS auf einer neuen Tabelle zu aktivieren?
Wenn du den Supabase-Client mit dem anon Key verwendest, ist jede Tabelle ohne RLS-Aktivierung zugänglich für jeden mit diesem Key. Das ist der größte Footgun in Supabase. Stelle einen CI-Check auf, der verifies, dass alle Tabellen im public-Schema RLS aktiviert haben. Du kannst pg_tables verknüpft mit pg_class abfragen, um diese Überprüfung zu automatisieren.
Kann ich RLS mit Supabase Edge Functions verwenden?
Ja. Edge Functions können einen Supabase-Client erstellen, entweder mit dem anon Key (RLS gilt) oder dem Service Role Key (RLS umgangen). Verwende den anon Key mit dem JWT des Benutzers für benutzerseitige Operationen, und den Service Role Key für administrative Aufgaben, die mandantenübergreifenden Zugriff benötigen.
Wie migriere ich von einer Single-Tenant-App zu Multi-Tenant?
Füge tenant_id als nullbare Spalte hinzu, backfill alle bestehenden Daten (alle Zeilen bekommen die gleiche Mandanten-ID, da es Single-Tenant war), dann füge den NOT-NULL-Constraint, Indizes und RLS-Richtlinien hinzu. Teste umfassend mit deinen bestehenden Daten, bevor du RLS aktivierst -- eine falsche Richtlinie kann alle deine Benutzer aussperren. Verwende Supabase's lokale Entwicklungsumgebung, um die Migration zu üben.