Multi-Tenant Next.js avec Supabase RLS : Guide Production
Pourquoi Supabase RLS pour la Multi-Tenancy
Soyons honnêtes : quand il s'agit de gérer la multi-tenancy dans les apps SaaS, vous avez des options. Vous pouvez créer une base de données distincte par tenant, ce qui semble être le nirvana organisationnel mais c'est coûteux et franchement difficile à gérer. Ou vous pouvez essayer des schémas séparés, ce qui est moins problématique opérationnellement mais toujours pas une promenade de santé quand il s'agit de migrations. Mais ensuite il y a le chouchou du monde SaaS — les tables partagées avec filtrage au niveau des lignes. Supabase rend cette approche très facile grâce à sa Row Level Security (RLS) native PostgreSQL.
Pourquoi cela a-t-il de l'importance ? Simple. Votre filtrage des données se fait au niveau de la base de données. Si vous plantez sur une clause WHERE dans votre route API Next.js, vous ne resterez pas éveillé la nuit en pensant aux violations de données, car la base de données elle-même est votre filet de sécurité. Et vraiment, à l'époque actuelle, ce n'est pas un luxe — c'est une nécessité.
Mais ne nous leurrons pas. RLS ajoute une surcharge à vos requêtes, complique le débogage et peut vous piéger pendant les migrations. Alors, comment les différentes approches de multi-tenancy se comparent-elles ?
| Approche | Niveau d'isolation | Coût | Complexité opérationnelle | Performance des requêtes |
|---|---|---|---|---|
| Base de données par tenant | Complète | Élevé ($50-200/tenant/mo) | Très élevée | Meilleure |
| Schéma par tenant | Forte | Moyen | Élevée (migrations) | Bonne |
| Tables partagées + RLS | Niveau ligne | Faible | Moyen | Bonne (avec réserves) |
| Filtrage au niveau applicatif | Aucun | Le plus faible | Faible | Meilleure |
Pour la plupart des produits SaaS avec moins de 10 000 tenants, les tables partagées avec RLS vous offrent le meilleur rapport qualité-prix. C'est ce dans quoi nous nous plongeons ici.

Modèles d'architecture : Partagé vs Isolé
Avant même de penser à écrire du code, vous devez choisir votre stratégie de résolution de tenant. En pratique, vous rencontrerez surtout deux approches :
Tenancy basée sur le sous-domaine
Vous avez déjà vu tenant-slug.yourapp.com ? Bienvenue au modèle le plus courant pour les SaaS B2B. C'est élégant, professionnel et rend la résolution de tenant dans un middleware très facile.
Tenancy basée sur le chemin
Celle-ci est votre basique /org/tenant-slug/dashboard. Plus facile à mettre en place car il n'y a pas de DNS wildcard, et cela fonctionne sur des plates-formes comme Vercel sans domaines personnalisés. Mais soyons honnêtes : cela ressemble un peu à porter des chaussettes avec des sandales. Nous recommandons généralement basé sur le sous-domaine pour les apps B2B en production et basé sur le chemin pour les outils internes ou les MVPs. Changer plus tard ? Vous maudirez votre moi passé — changer ces modèles n'est pas une blague.
Configuration du schéma de tenant
Voici un modèle de schéma qui ne nous a pas déçus en trois déploiements en production distincts :
-- Table tenant de base
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 '{}'
);
-- Table de jonction d'adhésion
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)
);
-- Exemple de table scoped au tenant
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 sur org_id — vous en aurez besoin sur CHAQUE table scoped au tenant
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);
La table memberships est la colle qui tient tout ensemble. Toutes vos politiques RLS y feront référence comme si c'était leur cousin préféré. Les utilisateurs peuvent rejoindre plusieurs organisations, et leurs rôles dictent ce qu'ils peuvent ou ne peuvent pas faire. Et voici un petit conseil de sagesse : toujours — sérieusement, toujours — indexer org_id sur chaque table scoped au tenant. Sinon, regardez vos requêtes ramper comme de la mélasse une fois que vous nagerez dans les données. Nous avons été surpris par cela quand le tableau de bord d'un client est passé de 50ms à 8 secondes avec 100 000 lignes. Leçon apprise.
Politiques RLS qui passent réellement à l'échelle
C'est là où les tutoriels s'inclinent généralement, vous laissant en détresse. Ils vous jettent auth.uid() = user_id et disent, « Bonne chance ! » Mais la RLS multi-tenant ne peut pas être réduite comme cela.
Le modèle de fonction d'aide
Pourquoi encombrer chaque politique avec des vérifications d'adhésion ? Utilisez plutôt une fonction d'aide :
-- Aide : vérifier si l'utilisateur actuel est un membre d'une 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;
-- Aide : obtenir le rôle de l'utilisateur dans une 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;
Pourquoi SECURITY DEFINER ? Parce que la fonction s'exécute avec les privilèges du créateur, contournant RLS sur la table memberships. Sans cela, vous risquez de tomber dans un terrier de dépendance circulaire où RLS sur memberships plante les vérifications d'adhésion sur lesquelles d'autres tables comptent.
Et la partie STABLE ? Cela signale au planificateur de requêtes que la sortie de la fonction reste cohérente pour la même entrée au cours d'une seule requête, permettant certains avantages de mise en cache. Tenté d'utiliser IMMUTABLE ? Ne faites pas cela. L'adhésion peut basculer entre les transactions.
Politiques pour les tables scoped au tenant
Regardons quelques politiques pour notre table projects :
-- Activer RLS
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- SELECT : les membres peuvent voir les projets dans leurs orgs
CREATE POLICY "Members can view org projects"
ON projects FOR SELECT
USING (public.is_member_of(org_id));
-- INSERT : les admins et propriétaires peuvent créer des projets
CREATE POLICY "Admins can create projects"
ON projects FOR INSERT
WITH CHECK (
public.get_role_in(org_id) IN ('owner', 'admin')
);
-- UPDATE : les admins et propriétaires peuvent mettre à jour les projets
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 : seuls les propriétaires peuvent supprimer les projets
CREATE POLICY "Owners can delete projects"
ON projects FOR DELETE
USING (
public.get_role_in(org_id) = 'owner'
);
Politiques pour la table Memberships elle-même
Celle-ci est délicate. La table memberships reçoit sa propre RLS, mais elle ne peut pas utiliser les fonctions d'aide car elles, à leur tour, interrogent memberships — cue cauchemars de référence circulaire :
ALTER TABLE memberships ENABLE ROW LEVEL SECURITY;
-- Les utilisateurs peuvent voir les adhésions dans les orgs auxquelles ils appartiennent
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()
)
);
-- Seuls les propriétaires peuvent ajouter des membres
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'
)
);
Oui, il y a une sous-requête sur la même table. Et oui, PostgreSQL l'écrase. La sous-requête vérifie votre propre adhésion, non affectée par la politique en cours de définition car RLS ne s'enroule autour que de la requête externe. Mais testez cela — sérieusement, vous ne voulez pas trouver un bug en production.

Middleware Next.js pour la résolution de tenant
Avec Next.js 15 et le magnifique App Router, le middleware s'exécutant en edge est le gestionnaire immobilier parfait pour la résolution de tenant. Voici notre modèle de confiance pour les configurations basées sur le sous-domaine :
// 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];
// Ignorer pour le domaine principal et 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).*)'],
};
L'en-tête x-tenant-slug est de l'or pur. Utilisez-le pour laisser vos Server Components et routes API savoir avec quel tenant ils traitent. Si vous collaborez avec nous sur un projet Next.js, configurer cela est notre priorité du jour un.
Flux d'authentification dans les apps multi-tenant
Supabase Auth joue la neutralité dans le jeu de la multi-tenancy. Les utilisateurs existent dans une sphère mondiale — les relations de tenant sont votre puzzle à résoudre. Voici notre plan de jeu :
- L'utilisateur s'inscrit : Créer un utilisateur auth, construire une organisation et créer une adhésion avec un rôle 'owner'.
- L'utilisateur est invité : L'admin crée une invitation en attente, un nouvel utilisateur rejoint via le lien d'invitation, et pouf — une adhésion apparaît avec le rôle spécifié.
- L'utilisateur se connecte : Extraire le tenant du sous-domaine, confirmer l'adhésion, l'escorter vers son tableau de bord.
// 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();
// S'inscrire l'utilisateur
const { data: authData, error: authError } = await supabase.auth.signUp({
email,
password,
});
if (authError) return NextResponse.json({ error: authError.message }, { status: 400 });
// Utiliser un client avec rôle de service pour la création d'org (contourne 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 });
// Créer l'adhésion propriétaire
await adminClient
.from('memberships')
.insert({
user_id: authData.user!.id,
org_id: org.id,
role: 'owner',
});
return NextResponse.json({ org });
}
Remarquez que nous comptons sur un client avec rôle de service lors de l'inscription. L'utilisateur n'a pas encore d'adhésions, donc RLS les laisserait en détresse pour la création d'organisation. C'est l'un de ces problèmes d'amorçage classiques — votre clé de rôle de service sera votre baguette magique.
Et je ne peux pas assez insister : Ne jamais, jamais exposer votre clé de rôle de service au client. C'est strictement pour le code côté serveur.
Server Components et RLS : Le problème SSR
Les Server Components de Next.js 15 sont liés au serveur, renforçant le jeu de sécurité. Mais il y a un problème quand on utilise Supabase RLS : vous devez fournir la session de l'utilisateur au client Supabase pour que les politiques RLS sachent qui est à la table.
// 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 {
// Cela peut échouer dans Server Components (lecture seule)
// Le middleware gère l'actualisation des cookies
}
},
},
}
);
}
// 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');
// Obtenir l'ID de l'org à partir du slug
const { data: org } = await supabase
.from('organizations')
.select('id')
.eq('slug', tenantSlug)
.single();
if (!org) return <div>Organization not found</div>;
// RLS filtre automatiquement — retourne uniquement les projets
// où l'utilisateur actuel a une adhésion
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>
);
}
Voici le truc : même si quelqu'un falsifie org_id dans la requête, RLS ne bougera pas. Il bloque l'accès aux projets à moins que l'utilisateur ne soit un membre. Techniquement, .eq('org_id', org.id) est redondant pour la sécurité — RLS gère cela — mais c'est bon pour la performance et la lisibilité.
Optimisation des performances et pièges courants
Le problème de la requête N+1 RLS
Chaque vérification de politique RLS lance une sous-requête. Se connecter à une vérification de politique de 10 lignes quand vous regardez 100 lignes signifie 100 tours de recherche d'adhésion. Heureusement, PostgreSQL est assez intelligent pour mettre en cache — mais il y a une surcharge.
Fix : Utilisez STABLE sur les fonctions d'aide (comme nous l'avons décrit). De plus, pensez à dénormaliser org_id dans les revendications JWT :
-- Crochet JWT personnalisé (Tableau de bord Supabase > 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;
Ensuite, votre politique RLS devient :
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[])
)
);
Cela élimine entièrement la recherche de la table d'adhésion. Les ID d'org viennent directement du JWT. Caveat : Les revendications JWT sont horodatées à la connexion. Changez l'adhésion de quelqu'un, et ils devront se réauthentifier pour synchroniser les revendications. Généralement, c'est tout à fait gérable — gardez-le dans vos docs.
Pooling de connexions
Supabase offre un pooling de connexions via PgBouncer. Si vous allez en production avec Next.js sur Vercel, n'oubliez pas : URL pooler pour les routes API et les composants serveur.
# Pour les opérations régulières (poolées)
DATABASE_URL=postgres://user:pass@db.project.supabase.co:6543/postgres
# Pour les migrations uniquement (direct)
DIRECT_URL=postgres://user:pass@db.project.supabase.co:5432/postgres
Quiconque sur Supabase Pro pour $25 par mois obtient 200 connexions simultanées via le pooler. Pour la plupart des apps SaaS avec moins de 1000 utilisateurs simultanés, c'est plus que suffisant.
Les index dont vous avez absolument besoin
Voici l'ensemble d'index par force brute pour une configuration multi-tenant :
-- Sur chaque table scoped au tenant
CREATE INDEX idx_{table}_org_id ON {table}(org_id);
-- Index composites pour les requêtes courantes
CREATE INDEX idx_projects_org_created ON projects(org_id, created_at DESC);
-- Memberships — fortement interrogés par 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 — le meilleur ami d'un développeur. Voyez comment vos requêtes se font avec RLS à bord. Vous pourriez avoir un choc sur ce que le planificateur décide de faire sans les bons index.
Test des politiques RLS
Tout le monde saute par-dessus cela, pourtant c'est votre meilleur filet de sécurité contre les fuites de données. Nous testons les politiques RLS directement en SQL :
-- Tester en tant qu'utilisateur spécifique
SET request.jwt.claims = '{"sub": "user-uuid-here", "role": "authenticated"}';
SET role = 'authenticated';
-- Cela devrait retourner uniquement les projets auxquels l'utilisateur a accès
SELECT * FROM projects;
-- Cela devrait échouer (utilisateur non membre de cette org)
INSERT INTO projects (org_id, name) VALUES ('other-org-uuid', 'Sneaky Project');
-- Réinitialiser
RESET role;
Et n'oublions pas pgTAP pour les politiques critiques :
BEGIN;
SELECT plan(3);
-- Configurer le contexte de test en tant qu'utilisateur A (membre de l'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;
Exécutez-les en CI. Chaque migration qui joue avec les politiques RLS devrait envoyer la suite de test complète à travers un entraînement vigoureux.
Liste de contrôle de déploiement en production
Prêt à expédier ? Préparez-vous avec ceci :
- RLS activé sur chaque table contenant des données de tenant
- Clé de rôle de service gardée côté serveur, nulle part près d'un client
-
org_idcorrectement indexé sur toutes les tables scoped au tenant - Les fonctions d'aide d'adhésion anoblies comme
SECURITY DEFINERetSTABLE - Revendications JWT personnalisées verrouillées et chargées (si sur la route JWT)
- Avez-vous le pooling de connexion configuré pour le déploiement cloud ?
- Politiques RLS fraîchement sorties de l'assurance qualité avec pgTAP ou similaire
- Avez-vous cranné
EXPLAIN ANALYZEsur les requêtes cruciales avec RLS fonctionnant - Le flux d'invite/d'inscription ne manque-t-il d'amorçages d'adhésion ?
- Une limite de débit sur les points de terminaison auth ? Supabase propose des options intégrées
- Avez-vous activé RLS pour les tables du schéma
authdans le tableau de bord Supabase (souvent un piège) - Surveillance intégrée pour les requêtes lentes (Tableau de bord Supabase > Database > Query Performance)
Lancer un produit multi-tenant et vouloir quelqu'un qui a traversé ces eaux ? Nos solutions de développement headless CMS ou une rapide conversation via notre page de contact pourraient être exactement ce qu'il vous faut.
FAQ
Puis-je utiliser Supabase RLS pour les apps avec des milliers de tenants ?
Absolument. Nous avons testé RLS de table partagée avec plus de 5 000 tenants et des millions de lignes sans transpirer. La sauce secrète ? Indexation appropriée sur les colonnes org_id et fonctions d'aide STABLE. Envisagez 50 000+ tenants ou des extravagances de milliards de lignes ? Plongez dans le partitionnement des tables par org_id ou flirtez avec une configuration de schéma par tenant.
Comment gérer le changement de tenant quand un utilisateur appartient à plusieurs organisations ?
Gardez l'organisation active dans un cookie ou une URL (sous-domaine). Changer d'org ? Modifiez le sous-domaine/cookie et récupérez anew. N'intégrez pas l'org active dans le JWT — cela demande une nouvelle connexion pour changer. Un cookie que votre middleware peut regarder est la voie à suivre.
Que se passe-t-il si j'oublie d'activer RLS sur une table ?
Chaque utilisateur authentifié pourrait accéder à chaque ligne. C'est la position par défaut de PostgreSQL — aucune retenue au niveau des lignes sur les tables sans RLS. Le tableau de bord Supabase signale les tables manquant RLS, mais intégrer cela dans CI avec des requêtes à pg_tables et pg_policies aide aussi.
Dois-je utiliser la clé de rôle de service de Supabase ou cuire un rôle PostgreSQL personnalisé pour les tâches administratives ?
Généralement, la clé de rôle de service suffît. Elle contourne RLS entièrement, donc c'est votre secret le plus important pour une utilisation côté serveur uniquement. Besoin d'une gouvernance granulaire (comme un rôle « admin » lurking dans toutes les orgs mais timide des suppressions) ? C'est le territoire PostgreSQL personnalisé — avancé et généralement hors de votre radar jusqu'à ce que les outils internes complexes l'exigent.
Comment exécuter les migrations de base de données sans trébucher sur les politiques RLS ?
Le CLI Supabase (supabase db push ou supabase migration) aux côtés de l'URL de base de données directe (contournant le pooled) a votre soutien. Glissez les modifications de politique RLS dans la même migration que les tweaks de schéma. Testez le cast des migrations contre un projet de staging — Supabase vous permet de tourner des branches d'aperçu sur Pro pour exactement ce genre de chose.
Les politiques RLS peuvent-elles atteindre les données d'autres API ou services ?
Non. Les politiques RLS sont bien au chaud en SQL, évaluées par PostgreSQL. Fancy vérifier les données externes (comme un service de drapeau de fonctionnalité) ? Cimentez ces données dans une table de base de données, puis référencez dans votre politique. Un modèle typique est la synchronisation des statuts d'abonnement de Stripe vers une colonne organizations.plan.
Quel est l'impôt de performance de RLS par rapport au filtrage au niveau applicatif ?
Là-bas dans nos repères Supabase Pro (2 vCPUs, 8GB RAM), RLS ajoute un supplément de 1-3ms par requête pour les politiques de vérification d'adhésion basiques avec les bons index. Allez sauvage avec la complexité de la politique ou les jointures et vous pourriez ajouter 5-15ms. La tactique de revendications JWT (stocker org_ids dans le token) la réduit en dessous de 1ms puisqu'il n'y a pas de danse de sous-requête. Pour les apps web typiques, ce filet de latence est négligeable.
Comment cela fonctionne-t-il avec les abonnements Supabase Realtime ?
Supabase Realtime respecte la règle RLS. Accordez-vous à des changements de table et capturez uniquement les événements de lignes pour lesquelles vous êtes admissible à voir selon RLS. Cela se déploie immédiatement sans bricolage supplémentaire. Assurez-vous simplement que votre Supabase côté client a la session utilisateur, que @supabase/ssr gère sans couture.