Diseñando un Esquema de Base de Datos Multi-Tenant: Patrones y Compromisos
La multi-tenancy es una de las decisiones arquitectónicas más consecuentes en el diseño de bases de datos SaaS. Aprende los tres patrones principales, cuándo usar cada uno y cómo implementar correctamente el aislamiento de inquilinos.
Cuando construyes una aplicación SaaS, una de las primeras decisiones arquitectónicas que tomarás es cómo manejar la multi-tenancy a nivel de base de datos. Esta elección afecta el aislamiento de datos, el rendimiento de consultas, la complejidad operacional, la postura de cumplimiento y cuán difícil será migrar inquilinos individuales en el futuro. Vale la pena dedicar tiempo significativo antes de escribir cualquier código de aplicación.
Hay tres enfoques fundamentales para el diseño de bases de datos multi-tenant, cada uno con un conjunto distinto de compromisos. La elección correcta depende de tu número de inquilinos, requisitos de cumplimiento, tamaño del equipo y trayectoria de crecimiento. ER Flow facilita diseñar y visualizar cualquier patrón que elijas antes de comprometerte con una implementación.
Enfoque 1: Tablas Compartidas con `tenant_id`
Este es el enfoque más común para productos SaaS de alto crecimiento. Cada tabla con alcance de inquilino obtiene una columna tenant_id — una clave foránea a una tabla central tenants. Todos los inquilinos comparten las mismas tablas, y el aislamiento de inquilinos se aplica a nivel de aplicación y consulta. El esquema se ve así: tenants(id, name, slug, plan_id, created_at), luego cada tabla de entidades agrega tenant_id NOT NULL REFERENCES tenants(id). Una tabla users se convierte en users(id, tenant_id, email, password_hash, created_at). Una tabla orders se convierte en orders(id, tenant_id, user_id, status, total_cents, created_at).
Ventajas: Simple de implementar, fácil de agregar nuevos inquilinos (solo inserta una fila en tenants), baja sobrecarga operacional y uso eficiente de los recursos de la base de datos. Una migración de esquema se aplica a todos los inquilinos simultáneamente. El escalado horizontal es sencillo — puedes fragmentar por tenant_id cuando una sola instancia de base de datos alcance sus límites.
Desventajas: Los errores a nivel de aplicación pueden accidentalmente exponer datos entre inquilinos. Un solo filtro WHERE tenant_id = ? faltante en una consulta es un incidente de seguridad potencial. Las consultas intensas de un inquilino grande pueden degradar el rendimiento para otros inquilinos que comparten la misma instancia de base de datos. Los clientes empresariales con estrictos requisitos de cumplimiento pueden rechazar este modelo completamente.
Enfoque 2: Esquema por Inquilino
En PostgreSQL, un "esquema" es un espacio de nombres dentro de una base de datos. Cada inquilino obtiene su propio esquema: tenant_acme.users, tenant_beta.users. La aplicación establece el search_path al esquema correcto en el momento de la conexión. Este enfoque proporciona un aislamiento significativamente más fuerte que las tablas compartidas — un error en el código de la aplicación necesitaría tanto omitir la configuración de search_path como de alguna manera acceder al esquema de otro inquilino.
Ventajas: Aislamiento fuerte sin el riesgo de filtración de datos entre inquilinos por errores de consulta. El esquema de cada inquilino puede tener una estructura ligeramente diferente si es necesario (útil durante despliegues graduales). Mover a un solo inquilino a infraestructura dedicada es sencillo — copia el esquema a la nueva instancia y actualiza el enrutamiento de conexiones.
Desventajas: PostgreSQL soporta muchos esquemas por base de datos, pero el rendimiento puede degradarse con miles de ellos. Las migraciones deben aplicarse a cada esquema de inquilino individualmente — tus herramientas de migración necesitan iterar sobre todos los esquemas de inquilinos, lo cual es más lento y difícil de revertir de forma segura. Las operaciones de copia de seguridad y restauración se vuelven más complejas. Este patrón también es específico de MySQL en su implementación; los esquemas de MySQL son equivalentes a bases de datos, haciendo que el esquema por inquilino sea efectivamente lo mismo que la base de datos por inquilino en ese motor.
Enfoque 3: Base de Datos por Inquilino
El enfoque más aislado: cada inquilino obtiene una instancia de base de datos completamente separada. La aplicación tiene una capa de enrutamiento que mapea tenant_id a la cadena de conexión correcta. Este es el modelo usado por productos SaaS empresariales con estrictos requisitos de cumplimiento — HIPAA, SOC 2 Tipo II, FedRAMP. Los datos de cada inquilino están físicamente separados de todos los demás.
Ventajas: Aislamiento perfecto — un incidente de base de datos que afecta a un inquilino tiene cero impacto en los demás. Puedes ejecutar diferentes versiones de base de datos, diferentes configuraciones o diferentes programas de copia de seguridad por inquilino. Hace que sea trivialmente fácil ofrecer a los inquilinos una opción de instancia dedicada a un nivel de precios premium. Las certificaciones de cumplimiento como HIPAA se vuelven sencillas de lograr y demostrar.
Desventajas: Enorme sobrecarga operacional. Las migraciones deben aplicarse a cada base de datos de inquilino, a menudo a través de un sistema de orquestación que reintenta fallos y rastrea el progreso. El pool de conexiones se vuelve complejo — un servidor con 1,000 inquilinos necesita una gestión sofisticada de conexiones. El costo escala linealmente: 1,000 inquilinos significa al menos 1,000 instancias de base de datos. Este patrón solo es viable con una automatización pesada y un equipo lo suficientemente grande para gestionarlo.
Seguridad a Nivel de Fila en PostgreSQL
La Seguridad a Nivel de Fila (RLS) de PostgreSQL te da el aislamiento de tablas compartidas aplicado a nivel de base de datos en lugar de a nivel de aplicación. Defines políticas de seguridad en cada tabla: ALTER TABLE orders ENABLE ROW LEVEL SECURITY, luego CREATE POLICY tenant_isolation ON orders USING (tenant_id = current_setting('app.current_tenant_id')::uuid). Tu aplicación establece la variable de sesión antes de cada consulta: SET LOCAL app.current_tenant_id = 'tenant-uuid-here'.
PostgreSQL entonces agrega automáticamente la condición de la política a cada consulta en esa tabla — incluso si el código de la aplicación olvida filtrar por tenant_id. Esto es lo mejor de ambos mundos: la simplicidad de las tablas compartidas con aislamiento aplicado por la base de datos. La desventaja es que RLS agrega una pequeña sobrecarga a cada consulta y requiere pruebas cuidadosas para garantizar que las políticas funcionen correctamente para todos los patrones de consulta incluyendo joins.
Indexación para Esquemas Multi-Tenant
Cada columna indexada en un esquema multi-tenant debe incluir tenant_id como la primera columna en el índice. En lugar de CREATE INDEX idx_orders_status ON orders(status), usa CREATE INDEX idx_orders_status ON orders(tenant_id, status). Esto es crítico para el rendimiento de las consultas: la base de datos usa el índice para encontrar todos los pedidos de un inquilino específico con un estado dado, en lugar de escanear todos los pedidos de todos los inquilinos.
Para referencias polimórficas (usadas comúnmente en registros de auditoría y feeds de actividad), el índice compuesto debe ser (tenant_id, auditable_type, auditable_id). tenant_id siempre va primero en los índices compuestos para tablas multi-tenant — es el filtro de mayor selectividad que elimina la mayor cantidad de filas en el paso de escaneo más temprano posible. En ER Flow, puedes agregar definiciones de índices directamente a cada tabla en el diagrama, haciendo tu estrategia de indexación visible y revisable junto con la estructura del esquema.
Eliminaciones en Cascada y Limpieza de Inquilinos
Cuando un inquilino cancela y necesitas eliminar todos sus datos, las eliminaciones en cascada son tu herramienta más confiable. Define ON DELETE CASCADE en cada clave foránea que referencia tenants(id). Esto significa que un solo DELETE FROM tenants WHERE id = ? se propagará por todo tu esquema — eliminando todos los usuarios, pedidos, facturas y otros registros con alcance de inquilino en el orden correcto de dependencias.
Prueba tu comportamiento de cascada exhaustivamente en un entorno de preparación antes de confiar en él en producción. Para inquilinos muy grandes, una eliminación en cascada puede convertirse en una transacción de larga ejecución que bloquea tablas e impacta a otros inquilinos. El enfoque más seguro es hacer un soft delete del inquilino primero (tenants.deleted_at = NOW()), luego eliminar por lotes los registros hijo en segundo plano a lo largo del tiempo, y finalmente eliminar permanentemente la fila del inquilino cuando todos los hijos se hayan limpiado. Documenta este flujo de trabajo de eliminación en tu diagrama ER usando la función de notas de ER Flow, para que el procedimiento operacional esté adjunto al diseño del esquema mismo.
Cualquiera que sea el enfoque que elijas, modélalo explícitamente en ER Flow antes de implementarlo. El diagrama visual hace que el patrón de aislamiento de inquilinos sea concreto — puedes ver de un vistazo si cada tabla tiene un tenant_id, si las claves foráneas se propagan correctamente desde la tabla tenants, y si los índices están correctamente estructurados para los patrones de consulta multi-tenant. Las decisiones de diseño que parecen abstractas en una discusión técnica se vuelven inmediatamente verificables en un canvas compartido.