Waarom Supabase RLS voor Multi-Tenancy

Laten we eerlijk zijn: wanneer het gaat om multi-tenancy in SaaS-apps, heb je opties. Je kunt een apart database per tenant draaien, wat klinkt als organisatorische hemel maar duur en heel pijnlijk is om te beheren. Of je kunt aparte schemas proberen, wat operationeel minder gedoe is maar nog steeds niet makkelijk als het op migraties aankomt. Maar dan is er de lievelingszoon van de SaaS-wereld—gedeelde tabellen met rij-niveau filtering. Supabase maakt deze aanpak moeiteloos dankzij zijn PostgreSQL-native Row Level Security (RLS).

Waarom maakt dat überhaupt uit? Simpel. Je gegevenfiltering gebeurt op databaseniveau. Als je een WHERE-clausule in je Next.js API-route verkloot, hoef je niet 's nachts wakker te liggen met gedachten aan datalekken, omdat de database zelf je vangnet is. En eigenlijk, tegenwoordig, is dat geen luxe—het is een noodzaak.

Maar laten we ons niet voorliegen. RLS voegt overhead toe aan je queries, bemoeilijkt debugging, en kan je binnenkomen tijdens migraties. Dus hoe vergelijken verschillende multi-tenancy-benaderingen?

Aanpak Isolatieniveau Kosten Operationele Complexiteit Query-Performance
Database per tenant Volledig Hoog ($50-200/tenant/mnd) Zeer Hoog Best
Schema per tenant Sterk Gemiddeld Hoog (migraties) Goed
Gedeelde tabellen + RLS Rijniveau Laag Gemiddeld Goed (met voorbehoud)
Filtering op toepassingsniveau Geen Laagst Laag Best

Voor de meeste SaaS-producten met minder dan 10.000 tenants geeft gedeelde tabellen met RLS je het beste rendement. Dat is wat we hier gaan uitdiepen.

Multi-Tenant Next.js met Supabase RLS: Productiegids

Architectuurpatronen: Gedeeld versus Geïsoleerd

Voordat je zelfs maar aan code denkt, moet je je tenant-resolutiestrategie kiezen. In het wild kom je vooral twee beesten tegen:

Op Subdomain Gebaseerde Tenancy

Ooit tenant-slug.yourapp.com gezien? Welkom bij het meest voorkomende patroon voor B2B SaaS. Het is glad, professioneel, en maakt tenant-resolutie in middleware een fluitje van een cent.

Op Pad Gebaseerde Tenancy

Dit is je basis /org/tenant-slug/dashboard. Gemakkelijker in te stellen omdat er geen wildcard DNS is, en het werkt op platforms als Vercel zonder aangepaste domeinen. Maar laten we eerlijk zijn: het voelt een beetje als sokken met sandalen dragen. We raden doorgaans subdomain-gebaseerd aan voor productie B2B-apps en pad-gebaseerd voor interne tools of MVP's. Later wisselen? Je vloekt op jezelf—deze patronen veranderen is geen kleinigheid.

De Tenant-Schema Instellen

Hier is een schemapatroon dat ons in drie verschillende productierollouts niet heeft laten stikken:

-- Core tenant-tabel
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 '{}'
);

-- Lidmaatschap-junctietabel
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)
);

-- Voorbeeld tenant-scoped tabel
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 op org_id — je hebt dit op ELKE tenant-scoped tabel nodig
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);

De memberships-tabel is de lijm die alles bij elkaar houdt. Al je RLS-beleidsregels wijzen ernaar alsof het hun favoriete neef is. Gebruikers kunnen lid zijn van meerdere organisaties, en hun rollen bepalen wat ze wel of niet kunnen doen. En hier is een beetje wijsheid: altijd—serieus, altijd—index org_id op elke tenant-scoped tabel. Anders kijken je queries toe hoe ze kruipen als melasse zodra je zwemt in data. We werden verrast toen het dashboard van een klant een duikvlucht maakte van 50ms naar 8 seconden met 100.000 rijen. Lering geleerd.

RLS-beleidsregels Die Echt Schalen

Hier is waar tutorials doorgaans buigen, je verlatend op het droge. Ze gooien auth.uid() = user_id naar je toe en zeggen: "Veel sterkte!" Maar multi-tenant RLS kan niet zo eenvoudig worden gekookt.

Het Helper-Functiepatroon

Waarom elke beleidsregel met lidmaatschapcontroles bevolken? Gebruik in plaats daarvan een helperfunctie:

-- Helper: controleer of huidige gebruiker lid is van een org
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: krijg gebruiker's rol in een 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;

Waarom SECURITY DEFINER? Omdat de functie draait met de privileges van de maker, RLS op de memberships-tabel overslaat. Zonder dit riskeert je in een circulaire-afhankelijkheids-konijnenhol te vallen waar RLS op memberships de lidmaatschapcontroles waar andere tabellen van afhangen, crasht.

En het STABLE gedeelte? Het signaleert aan de query planner dat de output van de functie consistent blijft voor dezelfde invoer tijdens één query, wat mooie caching-voordelen mogelijk maakt. Gek om IMMUTABLE te gebruiken? Nee. Lidmaatschap kan tussen transacties veranderen.

Beleidsregels voor Tenant-Scoped Tabellen

Laten we naar wat beleidsregels voor onze projects-tabel kijken:

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

-- SELECT: leden kunnen projecten in hun orgs bekijken
CREATE POLICY "Members can view org projects"
  ON projects FOR SELECT
  USING (public.is_member_of(org_id));

-- INSERT: admins en owners kunnen projecten aanmaken
CREATE POLICY "Admins can create projects"
  ON projects FOR INSERT
  WITH CHECK (
    public.get_role_in(org_id) IN ('owner', 'admin')
  );

-- UPDATE: admins en owners kunnen projecten bijwerken
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: alleen owners kunnen projecten verwijderen
CREATE POLICY "Owners can delete projects"
  ON projects FOR DELETE
  USING (
    public.get_role_in(org_id) = 'owner'
  );

Beleidsregels voor de Memberships-Tabel Zelf

Dit is lastig. De memberships-tabel krijgt zijn eigen RLS, maar kan de helperfuncties niet gebruiken omdat die op hun beurt memberships bevragen—cue circulaire-referentie nachtmerries:

ALTER TABLE memberships ENABLE ROW LEVEL SECURITY;

-- Gebruikers kunnen lidmaatschappen in orgs zien waar ze bij horen
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()
    )
  );

-- Alleen owners kunnen leden toevoegen
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, er is een subquery op dezelfde tabel. En ja, PostgreSQL slaagt erin. De subquery controleert je eigen lidmaatschap, onbewogen door het beleidsregel dat wordt gedefinieerd omdat RLS alleen om de buitenste query heen loopt. Maar test dit—serieus, je wilt geen bug in productie ontdekken.

Multi-Tenant Next.js met Supabase RLS: Productiegids - architectuur

Next.js Middleware voor Tenant-Resolutie

Met Next.js 15 en de schitterende App Router is middleware die aan de edge draait, de perfecte huisbaas voor tenant-resolutie. Hier is ons betrouwbare patroon voor op subdomain gebaseerde 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 voor main domein en 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).*)'],
};

De x-tenant-slug-header is pure goud. Gebruik het zodat je Server Components en API-routes weten met welke tenant ze te maken hebben. Als je met ons samenwerkt aan een Next.js project, stellen we dit op onze dag één op.

Verificatiestroom in Multi-Tenant Apps

Supabase Auth speelt het neutraal in het multi-tenancy-spel. Gebruikers bestaan in een globale sfeer—tenant-relaties zijn jouw puzzel om op te lossen. Hier is ons game plan:

  1. Gebruiker meldt zich aan: Maak een auth-gebruiker aan, bouw een organisatie en verwezenlijk een lidmaatschap met een 'owner'-rol.
  2. Gebruiker wordt uitgenodigd: De admin stelt een uitstekende uitnodiging op, een nieuwe gebruiker voegt zich via de invite-link in, en poef—een lidmaatschap verschijnt met de opgegeven rol.
  3. Gebruiker meldt zich aan: Haal tenant uit subdomain, bevestig lidmaatschap, escorteer hen naar hun 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();

  // Meld gebruiker aan
  const { data: authData, error: authError } = await supabase.auth.signUp({
    email,
    password,
  });

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

  // Gebruik een service role-client voor org-aanmaak (omzeilt 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 });

  // Maak ownership-lidmaatschap aan
  await adminClient
    .from('memberships')
    .insert({
      user_id: authData.user!.id,
      org_id: org.id,
      role: 'owner',
    });

  return NextResponse.json({ org });
}

Merk op dat we een service role-client gebruiken tijdens aanmelding. De gebruiker heeft nog geen lidmaatschappen, dus RLS zou hen in de steek laten voor organisatieaanmaak. Het is een van die klassieke bootstrappingproblemen—je service role-sleutel zal je toverstaf zijn.

En ik kan dit niet genoeg benadrukken: Geef je service role-sleutel nooit bloot aan de client. Het is strikt voor server-side-code.

Server Components en RLS: Het SSR-Probleem

Server Components van Next.js 15 zijn server-gebonden, waardoor het veiligheidsspel omhoog gaat. Maar er is een probleem wanneer je Supabase RLS gebruikt: je moet de sessie van de gebruiker aan de Supabase-client verstrekken zodat RLS-beleidsregels weten wie aan tafel zit.

// 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 {
            // Dit kan falen in Server Components (alleen-lezen)
            // De middleware verzorgt cookie-verversing
          }
        },
      },
    }
  );
}
// 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');

  // Haal org ID uit 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 — geeft alleen projecten terug
  // waar de huidige gebruiker lidmaatschap van heeft
  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 is het schot: zelfs als iemand de org_id in het verzoek knoeit, beweegt RLS zich niet. Het blokkeert toegang tot projecten tenzij de gebruiker een lid is. Technisch gezien is .eq('org_id', org.id) redundant voor beveiliging—RLS verzorgt dat—maar het is goed voor prestatie en leesbaarheid.

Prestatie-optimalisatie en Veelgebruikte Valkuilen

Het N+1 RLS-Query-Probleem

Elke RLS-beleidscontrole draait een subquery op. Haak aan in een 10-row beleidscontrole wanneer je naar 100 rijen kijkt betekent 100 ronden lidmaatschapopzoeking. Gelukkig is PostgreSQL slim genoeg om te cachen—maar daar zit overhead.

Oplossing: Gebruik STABLE op helperfuncties (zoals we hebben geschetst). Ook, denk erover na om org_id in de JWT-claims te denormaliseren:

-- 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;

Dan wordt je RLS-beleidsregel:

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[])
    )
  );

Dit vernietigt de lidmaatschap-tabel-opzoeking volledig. De org ID's komen rechtstreeks uit de JWT. Voorbehoud: JWT-claims zijn gestempeld bij aanmelding. Verander iemands lidmaatschap, en ze zullen opnieuw moeten verifiëren om claims te synchroniseren. Typisch is dat volkomen beheersbaar—zet het gewoon in je docs.

Verbindingsgroepering

Supabase levert verbindingsgroepering via PgBouncer. Als je live gaat met Next.js op Vercel, onthoud: pooler URL voor API-routes en server components.

# Voor normale operaties (gepooled)
DATABASE_URL=postgres://user:pass@db.project.supabase.co:6543/postgres

# Alleen voor migraties (direct)
DIRECT_URL=postgres://user:pass@db.project.supabase.co:5432/postgres

Iedereen op Supabase's Pro voor €25 per maand krijgt 200 gelijktijdige verbindingen via de pooler. Voor de meeste SaaS-apps verlegen met 1000 gelijktijdige gebruikers, is het meer dan genoeg.

Indexes Die Je Absoluut Nodig Hebt

Hier is de brute-force-index-set voor een multi-tenant-setup:

-- Op elke tenant-scoped tabel
CREATE INDEX idx_{table}_org_id ON {table}(org_id);

-- Samengestelde indexes voor veelgebruikte queries
CREATE INDEX idx_projects_org_created ON projects(org_id, created_at DESC);

-- Memberships — veel bevraagd door 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—een ontwikkelaar's beste vriend. Zie hoe je queries het doen met RLS aan boord. Je zou een onplezierige verrassing kunnen krijgen over wat de planner zonder de juiste indexes besluit te doen.

RLS-beleidsregels Testen

Iedereen slaat dit over, maar het is je beste vangnet tegen datalekken. We testen RLS-beleidsregels direct in SQL:

-- Test als een specifieke gebruiker
SET request.jwt.claims = '{"sub": "user-uuid-here", "role": "authenticated"}';
SET role = 'authenticated';

-- Dit zou alleen projecten moeten teruggeven waar de gebruiker toegang tot heeft
SELECT * FROM projects;

-- Dit zou moeten falen (gebruiker is geen lid van deze org)
INSERT INTO projects (org_id, name) VALUES ('other-org-uuid', 'Sneaky Project');

-- Reset
RESET role;

En vergeet pgTAP niet voor kritieke beleidsregels:

BEGIN;
SELECT plan(3);

-- Zet test-context op als gebruiker A (lid van 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;

Voer deze in CI uit. Elke migratie die RLS-beleidsregels aanraakt, zou de volledige testsuite een grondige training moeten geven.

Checklist Productieimplementatie

Klaar om te leveren? Bewapen jezelf met dit:

  • RLS ingeschakeld op elke tabel met tenant-gegevens
  • Service role-sleutel veilig gehouden aan server-kant, nergens in de buurt van een client
  • org_id correct geïndexeerd op alle tenant-scoped tabellen
  • Lidmaatschap-helperfuncties verheven tot SECURITY DEFINER en STABLE
  • JWT custom claims vastgezet en geladen (als op de JWT-route)
  • Verbindingsgroepering aangesloten voor cloud-implementatie?
  • RLS-beleidsregels vers uit QA-tests met pgTAP of diens soortgenoten
  • Opgedraamd EXPLAIN ANALYZE op cruciale queries met RLS actief
  • Invite/signup-stroom niet missend van lidmaatschapbootstraps
  • Rate limiting ingesteld op auth-eindpunten? Supabase biedt ingebouwde opties
  • De schakelaar omgegooid op RLS voor auth-schema-tabellen in het Supabase Dashboard (vaak een mijn)
  • Ingebouwde monitoring voor trage queries (Supabase Dashboard > Database > Query Performance)

Een multi-tenant product lanceren en wil je iemand die deze wateren al eerder heeft doorkruist? Onze headless CMS-ontwikkelingsoplossingen of een snelle chat via onze contactpagina zou precies kunnen zijn wat je nodig hebt.

Veelgestelde Vragen

Kan ik Supabase RLS gebruiken voor apps met duizenden tenants? Absoluut. We hebben gedeelde-tabel RLS gepilot met 5.000+ tenants en miljoenen rijen zonder zweet. Het geheim? Juiste indexering op org_id-kolommen en STABLE-helperfuncties. Denk je aan 50.000+ tenants of miljard-rij-extravaganzas? Duik in het partitioneren van tabellen op org_id of flirt met een schema-per-tenant-setup.

Hoe ga ik om met tenant-omschakeling wanneer een gebruiker tot meerdere organisaties behoort? Houd de actieve organisatie in een cookie of URL (subdomain). Wissel orgs? Wijzig het subdomain/de cookie en haal opnieuw op. Duw de actieve org niet in de JWT—het vraagt om opnieuw aan te melden om te veranderen. Een cookie waar je middleware naar kan kijken is de weg vooruit.

Wat gebeurt er als ik vergeet RLS op een tabel in te schakelen? Elke geverifieerde gebruiker zou elke rij kunnen tikken. Dat is PostgreSQL's standaardhouding—geen rij-niveau beperkingen op tabellen zonder RLS. Supabase Dashboard vlaggen tabellen die RLS missen, maar het inbedden ervan in CI met queries naar pg_tables en pg_policies helpt ook.

Moet ik de service role-sleutel van Supabase gebruiken of mijn eigen PostgreSQL-rol samenstellen voor admin-taken? Vooral is de service role-sleutel voldoende. Het omzeilt RLS geheel, dus het is je topgeheim voor server-side-gebruik alleen. Heb je fijnmazige governance nodig (zoals een "admin"-rol lurend in alle orgs maar verlegen voor verwijderingen)? Dat is aangepast PostgreSQL-terrein—geavanceerd en meestal niet op je radar tot complexe interne hulpmiddelen erom vragen.

Hoe voer ik databasemigraties uit zonder over RLS-beleidsregels te struikelen? Supabase's CLI (supabase db push of supabase migration) naast de directe databaseURL (pooling overslaan) heeft je rug. Stop RLS-beleidsregel-bewerkingen in dezelfde migratie als schema-aanpassingen. Test gietmigraties tegen een staging-project—Supabase laat je preview-branches opzetten op Pro voor precies dit soort ding.

Kunnen RLS-beleidsregels gegevens uit andere API's of services bereiken? Nee. RLS-beleidsregels zitten knus in SQL, geëvalueerd door PostgreSQL. Fancy controle op externe gegevens (zoals een feature-flag-service)? Cement die gegevens in een databasetabel, verwijs dan in je beleidsregel. Een typisch patroon is synchronisatie van abonnementstatussen van Stripe naar een organizations.plan-kolom.

Wat is de prestatiebelasting van RLS in vergelijking met filteren op toepassingsniveau? Over in onze Supabase Pro-benchmarks (2 vCPU's, 8GB RAM), bestrijkt RLS een extra 1-3ms per query voor basale lidmaatschapcontrole-beleidsregels met de juiste indexes. Ga wild met beleidsregel-complexiteit of joins en je zou 5-15ms kunnen toevoegen. De JWT-claims-tactiek (org_ids in het token opslaan) snijdt het onder 1ms omdat er geen subquery-dans is. Voor typische web-apps is dat trickle van latentie te verwaarlozen.

Hoe werkt dit met Supabase Realtime-abonnementen? Supabase Realtime speelt volgens de RLS-regelboeken. Stem af op tabelwijzigingen en vang alleen gebeurtenissen van rijen die je mag zien volgens RLS. Dit is meteen uit de doos met nul extra gedoe. Zorg gewoon dat je client-side Supabase de gebruikerssessie heeft, wat @supabase/ssr naadloos verzorgt.