Blog
7 min de lectura

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.

SeguridadMulti-tenantSaaSPostgreSQLSupabaseRow Level Security
𝕏in

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:

🗄️ Base de datos por cliente
  • Aislamiento total
  • Cara de mantener
  • Migraciones multiplicadas ×N
  • No escala más allá de 20-30 clientes
🏢 Base de datos compartida + RLS
  • 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.

❌ No hicimos
  • 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
✅ Sí hicimos
  • 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:

1
search_path no configurado — las funciones de BD podían ejecutarse en un schema inesperado, saltándose las políticas.
2
Políticas "always true" en producción — 3 tablas tenían políticas que permitían acceso sin restricción. Parecían seguras pero no verificaban tenant.
3
SECURITY DEFINER sin restricciones — funciones que se ejecutan con permisos elevados necesitan sus propios filtros de tenant, o abren una puerta trasera.
4
API routes con service_role sin validación — el service role salta RLS. Si una route lo usa sin verificar manualmente el tenant, cualquiera puede acceder a todo.

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:

🏥
Clínica
Pacientes, historiales, citas, facturas
Solo ve sus datos clínicos
🏭
Industrial
34 tablas de producción, calidad, RR.HH.
Solo ve sus datos industriales
📦
Distribuidora
Productos, lotes, stock, proveedores
Solo ve su inventario
💰
Usuarios individuales
Gastos, suscripciones, tarjetas, contraseñas
Solo ve sus finanzas personales

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:

1
Toda tabla nueva tiene tenant_id + RLS — sin excepciones. Si no tiene RLS, no se despliega.
2
Las funciones de BD siempre con search_path — para que nunca puedan ejecutarse en un contexto equivocado.
3
Service role = puertas blindadas — si una API route usa permisos elevados, incluye verificación manual de tenant_id. Doble check.
4
Auditoría de seguridad al cerrar cada sesión — revisamos routes sin auth, debug routes, secrets expuestos, RLS intacta. Siempre.
5
El middleware protege por defecto — todas las rutas API requieren autenticación. Solo las excepciones explícitas son públicas.

Los números

Tablas con RLS
80+
todas con tenant_id
Funciones con search_path
24
auditadas y corregidas
Filtraciones de datos
0
en 90+ sesiones de desarrollo
Empresas en producción
4
clínica + industrial + distribuidora + app B2C

¿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:

  1. Usa una sola base de datos con tenant_id en cada tabla. Es más barato, más mantenible y más escalable.
  2. Activa RLS desde el día 1. Añadirla después es un infierno. Diseñarla desde el principio es natural.
  3. 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.
  4. Audita periódicamente. Las políticas se quedan obsoletas, las funciones crecen, los permisos mutan. Revisión constante.
  5. Las funciones con permisos elevados necesitan su propia validación de tenant. SECURITY DEFINER + sin filtro = puerta trasera.

🛠️ Stack utilizado en este proyecto

PostgreSQL Supabase Row Level Security Next.js 16 TypeScript Middleware Auth SECURITY DEFINER

¿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.

¿Te ha resultado útil?

4.5 / 5 — 12 valoraciones

¿Estás montando un SaaS multi-tenant?

Te ayudamos a diseñar la arquitectura de datos segura desde el primer día. Sin parches — seguridad real desde la base.

Cuéntanos tu proyecto