ADR-001: Multi-Tenant Strategy
Estado
Section titled “Estado”Aceptada - 2024 (fecha estimada basada en implementación)
Contexto
Section titled “Contexto”El sistema Nostromo debe soportar múltiples empresas clientes (tenants) con requisitos estrictos de:
- Aislamiento de datos: Datos de un cliente NO deben ser accesibles por otro cliente bajo ninguna circunstancia
- Escalabilidad: Sistema debe soportar decenas o cientos de clientes simultáneamente
- Compliance: Cumplimiento de normativas de protección de datos (ej: GDPR, ley chilena de protección de datos)
- Independencia operacional: Cada cliente debe poder tener versiones de schema diferentes si es necesario (migraciones graduales)
Restricciones
Section titled “Restricciones”- Presupuesto limitado: No es viable mantener bases de datos separadas para cada cliente
- Complejidad operacional: El equipo es pequeño, se necesita solución mantenible
- Rendimiento: La solución debe ser eficiente (latencia baja, throughput alto)
Decisión
Section titled “Decisión”Implementar schema-per-tenant en PostgreSQL:
- Cada cliente (tenant) tiene su propio schema dentro de una única base de datos PostgreSQL
- El schema contiene todas las tablas de negocio del cliente (employees, contracts, payroll, etc.)
- El tenant ID se resuelve al inicio de cada request y se asigna el schema correspondiente vía
SET search_path - Un schema especial
centralcontiene metadatos de tenants, usuarios, y configuración global
Implementación
Section titled “Implementación”-- Ejemplo de estructuraCREATE SCHEMA tenant_empresa_123; -- Schema para empresa ABCCREATE SCHEMA tenant_empresa_456; -- Schema para empresa XYZCREATE SCHEMA central; -- Metadatos globales
-- Cada request ejecuta:SET search_path TO tenant_empresa_123;-- Ahora todas las queries usan automáticamente ese schemaComponentes clave:
Mothercontainer: Base de datos PostgreSQL 16 multi-tenanttenantResolver.ts: Resuelve tenant desde JWT/request → nombre de schemagetTenantPool(): Obtiene pool de conexiones y configura schema
Consecuencias
Section titled “Consecuencias”Positivas
Section titled “Positivas”✅ Aislamiento completo de datos
- Separación a nivel de schema garantiza que
SELECT * FROM employeesde la empresa A nunca retorna datos de empresa B - No hay riesgo de “data leakage” por olvidar
WHERE tenant_id = ?en queries
✅ Escalabilidad horizontal
- Posibilidad futura de distribuir schemas a diferentes servidores PostgreSQL usando Foreign Data Wrappers (FDW)
- Si tenant_empresa_123 crece mucho, se puede mover a servidor dedicado sin cambiar código
✅ Migrations independientes
- Cada tenant puede estar en versión diferente de schema
- Permite migraciones graduales por cliente (importante para sistemas críticos)
- Rollback de un tenant no afecta a otros
✅ Simplicidad operacional vs database-per-tenant
- Un único servidor PostgreSQL para gestionar (backups, monitoring, tuning)
- Conexiones compartidas en pool central (más eficiente)
✅ Performance aceptable
SET search_pathes operación muy rápida en PostgreSQL (~0.1ms)- Queries ejecutan igual que en base de datos dedicada
- Índices, statistics, query planner funcionan normalmente
Negativas (Trade-offs)
Section titled “Negativas (Trade-offs)”❌ Complejidad de connection pooling
- Requiere gestión de pool con schema-switching por request
- No se puede usar pool estándar “listo para usar” sin configuración
- Mitigación: Implementamos pool central en Mother con lógica de schema switching
❌ Overhead de migrations
- Migración de schema debe ejecutarse N veces (una por tenant)
- Si hay 50 tenants, un
ALTER TABLEse ejecuta 50 veces - Mitigación: Scripts de migración automatizan ejecución multi-tenant
❌ PostgreSQL dependency fuerte
- Solución está acoplada a PostgreSQL (schemas son feature de Postgres)
- Migrar a MySQL/MongoDB requeriría rediseño completo
- Mitigación: PostgreSQL es estable, maduro, open-source - riesgo aceptable
❌ Límite teórico de escalabilidad
- PostgreSQL tiene límite práctico de ~1000 schemas por DB
- Si se superan 1000 clients, se necesita múltiples DBs
- Mitigación: Estrategia de sharding cross-database vía FDW está documentada
❌ Backup complejo
- Un backup contiene todos los tenants (no aislados)
- Restaurar UN solo tenant requiere extracción selectiva
- Mitigación:
pg_dump --schema=tenant_Xpermite dumps por tenant
Alternativas Consideradas
Section titled “Alternativas Consideradas”Opción A: Database-per-tenant
Section titled “Opción A: Database-per-tenant”Descripción: Cada cliente tiene su propia base de datos PostgreSQL completa.
Rechazada porque:
- Costos de infraestructura: 100 clientes = 100 bases de datos = recursos x100
- Complejidad operacional: Gestionar 100 servidores DB (backups, monitoring, tuning, upgrades)
- Conexiones insostenibles: Cada DB necesita su pool de conexiones → explosión de recursos
- No escala para modelo SaaS: Agregar nuevo cliente requiere provisionar DB completa (lento)
Cuándo sería válida: Si cada cliente paga por infraestructura dedicada (no SaaS), o hay requisitos legales de aislamiento físico.
Opción B: Single-schema con tenant_id
Section titled “Opción B: Single-schema con tenant_id”Descripción: Todas las tablas tienen columna tenant_id, se filtran queries con WHERE tenant_id = ?.
Ejemplo:
SELECT * FROM employees WHERE tenant_id = 123;Rechazada porque:
- Riesgo CRÍTICO de data leakage: Un
WHERE tenant_idolvidado expone datos de todos los clientes- En JOIN complejos, fácil olvidar filtro en UNA tabla
- Bug en código → brecha de seguridad catastrófica
- Migrations difíciles: No puedes migrar UN tenant sin afectar a todos
- Performance degradado: Índices contienen datos de TODOS los tenants, menos eficientes
- Compliance problemático: Auditores prefieren aislamiento físico/lógico claro
Cuándo sería válida: Si requisitos de aislamiento son bajos (ej: multi-tenant “soft” donde clientes comparten ciertos datos).
Opción C: Híbrido (schema-per-tenant + shared tables)
Section titled “Opción C: Híbrido (schema-per-tenant + shared tables)”Descripción: Tablas de negocio en schemas separados, tablas comunes (parámetros SII, normativa) en schema shared.
Considerada pero no implementada inicialmente:
- Añade complejidad sin beneficio claro para v1
- Parámetros SII cambian poco, duplicar en cada schema es aceptable
- Reservado para futuro: Si storage de parámetros se vuelve problemático
Referencias
Section titled “Referencias”Implementación:
- Mother PostgreSQL Container - Setup de PostgreSQL multi-tenant
- Tenant Resolver - Lógica de resolución de tenant
- Pool Management - Connection pool con schema switching
Decisiones relacionadas:
- ADR-002: Pool Management - Cómo gestionar conexiones de DB
- Arquitectura Overview - Visión general del sistema
Referencias externas:
Notas Adicionales
Section titled “Notas Adicionales”Lecciones Aprendidas
Section titled “Lecciones Aprendidas”Durante la implementación descubrimos:
- Schema switching es rápido: Temor inicial de overhead de
SET search_pathresultó infundado - mediciones muestran ~0.1ms - Migrations requieren scripting robusto: Primera migración multi-tenant fue manual y propensa a errores - ahora automatizada
- Monitoring por tenant es valioso: Añadimos métricas por schema (storage, query time) para detectar clientes “pesados”
Consideraciones Futuras
Section titled “Consideraciones Futuras”Si se supera escala actual (~100-200 tenants):
- Sharding cross-database: Usar FDW para distribuir schemas a múltiples servidores PostgreSQL
- Read replicas por tenant: Clientes grandes pueden tener read replica dedicada
- Migración a Citus: Extensión PostgreSQL para sharding nativo multi-tenant
Próxima revisión: Cuando se alcance 80% del límite práctico de schemas (~800 clientes).