SaaS multiinquilino con seguridad a nivel de fila en Postgres
Si estás construyendo software B2B, estás construyendo un sistema multiinquilino lo llames así o no. Muchas empresas usan tu app, y los datos de cada una tienen que quedar completamente separados de los de las demás. Acertar en ese aislamiento no es un lujo. Una sola fuga entre inquilinos es el tipo de incidente que termina contratos. Este es el patrón en el que más confiamos, y por qué.
El enfoque ingenuo es poner una columna tenant_id en cada tabla y agregar WHERE tenant_id = ? a cada consulta. Funciona hasta el día en que un desarrollador olvida esa cláusula en una consulta. Y ahora la Empresa A puede ver las facturas de la Empresa B. El problema es que la seguridad depende de que cada consulta, sin excepción, se escriba perfecta, para siempre. Esa es una apuesta que tarde o temprano pierdes.
Deja que la base de datos lo imponga
Postgres tiene una función hecha exactamente para esto: la seguridad a nivel de fila (RLS). En lugar de confiar en que el código de la aplicación filtre las filas, le dices a la base de datos misma que una conexión solo puede ver filas que pertenecen a su inquilino actual. El filtro se aplica automáticamente a cada consulta, sean selects, updates o deletes. Y no hay forma de escribir una consulta que se le escape.
-- Activa RLS y define la regla una vez, por tabla
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON invoices
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- Después, en cada petición, tu app declara quién está preguntando:
SET app.tenant_id = '...el-inquilino-con-sesion-iniciada...';A partir de ahí, un simple SELECT * FROM invoices solo devuelve las filas del inquilino actual. Una cláusula WHERE olvidada deja de ser un hueco de seguridad. Es solo una consulta. La regla vive en un solo lugar, impuesta por el motor y no repartida entre miles de líneas de código.
La meta es hacer que una fuga entre inquilinos sea imposible por construcción, no simplemente improbable si todos programan con cuidado.
Lo único en lo que no puedes fallar
RLS es tan bueno como el valor que fijas para el inquilino actual. Ese SET app.tenant_id tiene que venir de la sesión verificada en el servidor. Nunca de algo que el cliente pueda enviar. Fíjalo al inicio de cada petición, justo después de autenticar al usuario, dentro de la misma conexión y la misma transacción que atiende esa petición. Con un pool de conexiones, reinícialo entre usos para que una petición no herede el inquilino de otra.
Cuándo ir más lejos
Para la mayoría de los productos SaaS, tablas compartidas con RLS es el punto dulce: simple de operar, barato de escalar y fuertemente aislado. Algunos negocios necesitan más: un esquema o incluso una base de datos por inquilino. Normalmente porque un cliente empresarial grande exige separación física, o porque el cumplimiento normativo lo requiere. Pero eso agrega un costo operativo real (las migraciones en cientos de esquemas se vuelven dolorosas), así que recurre a ello solo cuando un cliente o una regulación de verdad lo pidan, no por defecto.
La conclusión
El aislamiento entre inquilinos lo debe imponer la base de datos, no la memoria de filtrar cada consulta. La seguridad a nivel de fila de Postgres te da eso con una política única por tabla y un ajuste de inquilino por petición, controlado por la sesión del servidor. Constrúyelo desde el primer día. Adaptar el aislamiento a una app multiinquilino que ya está en producción es mucho más difícil que empezar con él. Si estás diseñando una plataforma B2B y la quieres bien hecha, hablemos, o mira lo que hemos construido.
¿Estás pensando en construir esto?
Appluex diseña y lanza apps móviles y web en producción, incluidas funciones de inteligencia artificial. Hablemos.