Un cliente nos preguntó: '¿puede otro cliente ver mis datos?' — Cómo protegemos una base de datos compartida con RLS
Cuatro empresas. Una sola base de datos. Cero posibilidad de que una vea los datos de otra. No es un eslogan — es Row Level Security bien implementada. Así lo hacemos en producción.
Estábamos en una reunión con un nuevo cliente — una empresa industrial de Barcelona — cuando nos hizo la pregunta que todo fundador de SaaS debería hacerse:
"Si compartimos plataforma con otros clientes… ¿puede alguno de ellos ver mis datos?"
La respuesta corta: no. La respuesta larga es este artículo.
Llevamos más de 90 sesiones de desarrollo construyendo una plataforma SaaS que usan simultáneamente una clínica, una distribuidora médica, una empresa industrial y cientos de usuarios individuales. Todos en la misma base de datos. Ninguno puede ver ni una fila que no le pertenezca.
4 empresas. 80+ tablas. 0 filtraciones.
La seguridad no es un feature — es la arquitectura.
El problema: una base de datos, muchos clientes
Cuando construyes un SaaS, tienes dos opciones para separar los datos de tus clientes:
- Aislamiento total
- Cara de mantener
- Migraciones multiplicadas ×N
- No escala más allá de 20-30 clientes
- Aislamiento a nivel de fila
- Un solo deploy, una sola migración
- Escala a miles de clientes
- Requiere disciplina y conocimiento
Nosotros elegimos la segunda. Una sola base de datos PostgreSQL con Row Level Security (RLS) — la misma que usan empresas como Notion, Figma y Supabase internamente.
Pero elegirla es la parte fácil. Implementarla bien es donde la mayoría se equivoca.
¿Qué es RLS y por qué debería importarte?
Row Level Security es una funcionalidad nativa de PostgreSQL que permite definir quién puede ver o modificar cada fila de cada tabla, directamente en la base de datos. No en tu código. No en tu API. En la propia base de datos.
Esto es importante. Si la seguridad solo está en tu código, un solo bug la rompe. Si está en la base de datos, ni siquiera un error en tu aplicación puede filtrar datos.
El error más peligroso en SaaS
Filtrar datos de un cliente a otro no es solo un bug — es un incidente de seguridad que puede destruir tu empresa. Con RLS, es físicamente imposible que una consulta SQL devuelva datos de otro tenant, aunque tu código tenga errores.
Cómo funciona en la práctica
El concepto es sencillo: cada tabla tiene una columna tenant_id. Cada fila pertenece a un cliente. Y PostgreSQL se encarga de que solo veas las tuyas.
// 💚 Así se ve una política RLS básica
CREATE POLICY "Los usuarios solo ven datos de su empresa"
ON pacientes
FOR ALL
USING (tenant_id = get_user_tenant_id())
// 🔒 Resultado: SELECT * FROM pacientes
// La clínica ve sus pacientes. La industrial ve 0 filas.
// No hay WHERE. No hay filtro. PostgreSQL lo hace solo.
Fíjate: no hay WHERE tenant_id = ... en la consulta. La aplicación hace un SELECT * normal y PostgreSQL aplica el filtro automáticamente. Es transparente, invisible e inviolable.
Lo que NO hicimos
Antes de hablar de lo que sí hicimos, es importante decir lo que no hicimos — porque son errores que vemos constantemente en proyectos de otros equipos.
- Filtrar por tenant_id en el frontend
- Confiar en que el API siempre filtra bien
- Usar una base de datos por cliente
- Desactivar RLS "para ir más rápido"
- Hardcodear IDs de tenant en el código
- RLS activada en todas las tablas desde el día 1
- tenant_id obligatorio en cada INSERT
- Funciones de BD con SECURITY DEFINER
- Auditoría de políticas cada 10 sesiones
- Tests reales cruzando datos entre tenants
Las trampas que descubrimos (y que no están en ningún tutorial)
Implementar RLS básica es fácil. Implementarla en producción con 80+ tablas y 4 empresas reales es donde aparecen los problemas. Estos son los que encontramos nosotros:
Cada uno de estos problemas fue detectado y corregido durante auditorías internas de seguridad que hacemos al final de cada sesión de desarrollo. No los encontró un hacker. Los encontramos nosotros antes de que importara.
El resultado: aislamiento real con 4 empresas en producción
Hoy nuestra plataforma tiene 4 empresas activas, cada una con su propio módulo y sus propios datos. Así se ve el aislamiento:
Todas comparten la misma infraestructura, el mismo deploy, la misma base de datos. Pero si la clínica hace un SELECT * FROM dist_products, obtiene 0 filas. No un error — simplemente no ve nada. Para PostgreSQL, esos datos no existen para ella.
Las reglas que seguimos siempre
Después de más de 90 sesiones perfeccionando esto, tenemos un protocolo que no rompemos nunca:
Los números
¿Por qué no lo hace todo el mundo?
Porque RLS bien hecha requiere pensar en seguridad desde la primera línea de código. No es algo que añades al final. No es un middleware. No es un plugin.
La mayoría de equipos hace esto:
// ❌ Seguridad en la aplicación — frágil
app.get("/api/patients", (req, res) => {
const patients = db.query(
"SELECT * FROM patients WHERE tenant_id = ?",
[req.user.tenantId] // 🤞 espero no olvidarme el WHERE
)
})
¿Ves el problema? Si alguien se olvida del WHERE en una sola ruta, todos los datos de todos los clientes quedan expuestos.
Con RLS, eso no puede pasar:
// 💚 Seguridad en la base de datos — inviolable
app.get("/api/patients", (req, res) => {
const patients = db.query("SELECT * FROM patients")
// 🔒 PostgreSQL aplica RLS automáticamente
// Solo devuelve los pacientes del tenant del usuario
// Aunque la query no tenga WHERE
})
La seguridad no depende de que el desarrollador escriba bien cada query. Depende de que la base de datos jamás devuelva lo que no debe.
TL;DR para los impacientes
Si estás construyendo un SaaS multi-tenant — da igual el tamaño — esto es lo que necesitas saber:
- Usa una sola base de datos con
tenant_iden cada tabla. Es más barato, más mantenible y más escalable. - Activa RLS desde el día 1. Añadirla después es un infierno. Diseñarla desde el principio es natural.
- No confíes en tu código para filtrar datos. Confía en PostgreSQL. Tu código puede tener bugs. Las políticas RLS no.
- Audita periódicamente. Las políticas se quedan obsoletas, las funciones crecen, los permisos mutan. Revisión constante.
- Las funciones con permisos elevados necesitan su propia validación de tenant. SECURITY DEFINER + sin filtro = puerta trasera.
🛠️ Stack utilizado en este proyecto
¿Estás diseñando un SaaS y no sabes cómo separar los datos de tus clientes? En Nexus Code diseñamos arquitecturas multi-tenant seguras desde el primer día — con PostgreSQL, Supabase y Row Level Security en producción real. Cuéntanos tu proyecto y te ayudamos a construirlo bien desde la base.