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.
Las guardas de aplicación y las políticas de base de datos resuelven fallos distintos
A menudo se trata la autorización a nivel de aplicación y Postgres Row-Level Security como opciones excluyentes. En la práctica, detectan errores distintos: las guardas del servidor aplican jerarquía de roles, comprobaciones de derechos y registros de auditoría antes de ejecutar la lógica de negocio, mientras que RLS garantiza que olvidar una cláusula WHERE workspace_id = ... nunca exponga las filas de todos los clientes.
Tanto si usas Supabase como Postgres autoalojado con Drizzle, cada tabla perteneciente a un cliente debe incluir una clave foránea workspace_id no nula. El artículo sobre límites de workspace explica por qué ese alcance debe derivarse siempre en el servidor.
- Las guardas de aplicación validan la vigencia de la sesión, los roles (
owner,admin,member) y los derechos de funciones. - Las políticas RLS de la base de datos actúan como un suelo de seguridad firme ante consultas directas de cliente o ayudantes SQL en bruto.
- Ambas capas dependen de una única fuente de verdad: la tabla de unión
workspace_members.
Resuelve la membresía en vivo en lugar de confiar en cookies obsoletas
Guardar un currentWorkspaceId en una cookie del navegador o cabecera de cliente resulta cómodo para navegar, pero una cookie es una preferencia, no un permiso. Si un administrador elimina a un compañero de un workspace, el navegador de ese usuario conserva la cookie antigua.
Cruza la sesión activa con workspace_members en cada petición. Cuando la fila de membresía ya no exista, rechaza la solicitud de inmediato en lugar de esperar a que caduque un JWT o una cookie de sesión. El artículo sobre autenticación más allá del formulario de acceso detalla cómo fluye la identidad hacia esta comprobación.
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 };
}Escribe políticas de Row-Level Security rápidas e indexadas
Las políticas RLS ingenuas que llaman a auth.uid() directamente en cada fila obligan a Postgres a reevaluar la función por cada tupla. Envolver (select auth.uid()) en una subconsulta escalar permite al planificador almacenar el valor en caché como un initPlan, y un índice compuesto sobre (workspace_id, user_id) mantiene las búsquedas de membresía por debajo del milisegundo.
Puedes generar políticas SELECT, INSERT, UPDATE y DELETE listas para ejecutar en tablas multi-tenant con el generador gratuito de políticas RLS para Supabase sin escribir SQL repetitivo a mano.
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')
)
);Preguntas frecuentes sobre RLS y autenticación de workspace
Los equipos que configuran Postgres multi-tenant suelen encontrarse con los mismos casos límite en torno a roles de servicio, trabajos en segundo plano y rendimiento.
- ¿Drizzle ORM o Prisma se saltan RLS? — Sí, si tu servidor se conecta con el superusuario
postgreso la claveservice_rolede Supabase. Por eso las server actions y rutas API deben llamar arequireWorkspaceViewer()y acotar cada consulta porworkspaceId. - ¿Por qué envolver
(select auth.uid())entre paréntesis dentro de las políticas RLS? — Indica al optimizador de Postgres que evalúeauth.uid()una sola vez por sentencia en lugar de una vez por fila escaneada, evitando escaneos secuenciales lentos en tablas grandes. - ¿Cómo deben acceder los trabajos en segundo plano a los datos del cliente? — Pasa el
workspaceIdverificado en la carga útil del trabajo desde la ruta autenticada del servidor y limita las lecturas y escrituras del worker a eseworkspaceId. - ¿Cómo verifico que el aislamiento entre clientes realmente se cumple? — Escribe pruebas negativas automatizadas en las que el Usuario A solicite o modifique el ID de registro del Usuario B en rutas web, móviles y API, y comprueba que devuelvan
403/404con cero filas modificadas.