Should I use Postgres Row-Level Security or application-level workspace checks?
It depends on how your application connects to Postgres. If browser or mobile clients query the database directly through PostgREST, Row-Level Security (RLS) is mandatory. If all traffic flows through a server-side TypeScript boundary, explicit membership checks on every query are your primary defense, and RLS serves as defense-in-depth against forgotten WHERE clauses. Combine live session-to-membership resolution with indexed workspace_id policies. supastack enforces server-side workspace guards across web, mobile, and desktop, and provides a free RLS policy generator for Supabase tables.
Les gardes applicatifs et les politiques de base de données corrigent des pannes différentes
On oppose souvent l'autorisation applicative et la sécurité au niveau des lignes (RLS) de Postgres. En pratique, elles interceptent des erreurs différentes : les gardes côté serveur appliquent la hiérarchie des rôles, les vérifications de droits et le journal d'audit avant l'exécution de la logique métier, tandis que RLS garantit qu'une clause WHERE workspace_id = ... oubliée ne divulguera jamais les lignes de tous les clients.
Que vous utilisiez Supabase ou un Postgres auto-hébergé avec Drizzle, chaque table appartenant à un client doit porter une clé étrangère non nulle workspace_id. L'article sur les frontières de workspace explique pourquoi ce périmètre doit toujours être déduit côté serveur.
- Les gardes applicatifs valident la fraîcheur de la session, les rôles des membres (
owner,admin,member) et les droits fonctionnels. - Les politiques RLS de la base de données servent de filet de sécurité absolu lors de requêtes clientes directes ou d'assistants SQL bruts.
- Les deux couches s'appuient sur une source de vérité unique : la table de jointure
workspace_members.
Vérifiez l'appartenance en direct plutôt que de faire confiance à des cookies périmés
Stocker un currentWorkspaceId dans un cookie de navigateur ou un en-tête client est pratique pour la navigation, mais un cookie est une préférence, pas une autorisation. Si un administrateur retire un collaborateur d'un workspace, le navigateur de cet utilisateur conserve l'ancien cookie.
Joignez la session active à workspace_members à chaque requête. Si la ligne d'appartenance a disparu, rejetez immédiatement la requête au lieu d'attendre l'expiration d'un JWT ou d'un cookie de session. L'article sur l'authentification au-delà du formulaire de connexion retrace la manière dont l'identité alimente cette vérification.
export async function requireWorkspaceViewer(requestedWorkspaceId?: string) {
const session = await requireAuthenticatedSession();
const membership = await db.query.workspaceMembers.findFirst({
where: and(
eq(workspaceMembers.userId, session.user.id),
requestedWorkspaceId
? eq(workspaceMembers.workspaceId, requestedWorkspaceId)
: undefined,
),
with: { workspace: true },
});
if (!membership) throw new ForbiddenError("Not a member of this workspace");
return { user: session.user, workspace: membership.workspace, role: membership.role };
}Écrivez des politiques Row-Level Security rapides et indexées
Les politiques RLS naïves qui appellent auth.uid() directement sur chaque ligne forcent Postgres à réévaluer la fonction pour chaque tuple. Envelopper (select auth.uid()) dans une sous-requête scalaire permet au planificateur de mettre la valeur en cache sous forme d'initPlan, et un index composite sur (workspace_id, user_id) maintient les recherches d'appartenance sous la milliseconde.
Vous pouvez générer des politiques SELECT, INSERT, UPDATE et DELETE prêtes à l'emploi pour vos tables multi-tenant grâce au générateur gratuit de politiques RLS Supabase sans écrire de SQL répétitif à la main.
CREATE INDEX IF NOT EXISTS idx_workspace_members_lookup
ON workspace_members (workspace_id, user_id);
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
CREATE POLICY "workspace_members_manage_documents"
ON documents FOR ALL TO authenticated
USING (
workspace_id IN (
SELECT wm.workspace_id FROM workspace_members wm
WHERE wm.user_id = (SELECT auth.uid())
)
)
WITH CHECK (
workspace_id IN (
SELECT wm.workspace_id FROM workspace_members wm
WHERE wm.user_id = (SELECT auth.uid())
AND wm.role IN ('owner', 'admin', 'member')
)
);Questions fréquentes sur RLS et l'authentification de workspace
Les équipes qui mettent en place un Postgres multi-tenant rencontrent les mêmes cas limites autour des rôles de service, des tâches de fond et des performances.
- Drizzle ORM ou Prisma contournent-ils RLS ? — Oui, si votre serveur se connecte avec le superutilisateur
postgresou la cléservice_rolede Supabase. C'est pourquoi les server actions et les routes API doivent appelerrequireWorkspaceViewer()et filtrer chaque requête parworkspaceId. - Pourquoi entourer
(select auth.uid())de parenthèses dans les politiques RLS ? — Cela indique à l'optimiseur Postgres d'évaluerauth.uid()une seule fois par requête plutôt qu'une fois par ligne parcourue, évitant ainsi des parcours séquentiels lents sur les grandes tables. - Comment les tâches de fond doivent-elles accéder aux données d'un client ? — Transmettez le
workspaceIdvérifié dans la charge utile du job depuis la route serveur authentifiée, et limitez les lectures et écritures du worker à ceworkspaceId. - Comment vérifier que l'isolation entre clients tient réellement ? — Écrivez des tests négatifs automatisés où l'Utilisateur A demande ou modifie l'identifiant d'enregistrement de l'Utilisateur B sur les routes web, mobiles et API, et vérifiez que la réponse est
403/404sans aucune ligne modifiée.