Arquitectura Multi-tenant desde el Día 1
Nuestras decisiones de diseño iniciales para soportar múltiples clientes de forma segura y escalable, usando PostgreSQL y Supabase.
Ana Martínez
CTO & Co-founder
Por Qué Multi-tenant desde el Inicio
Una de las decisiones más importantes que tomamos fue diseñar InfraUX como una aplicación multi-tenant desde el primer día. Esta decisión, aunque agregó complejidad inicial, ha sido fundamental para nuestro crecimiento y escalabilidad.
¿Qué es Multi-tenancy?
Multi-tenancy es un patrón de arquitectura donde una única instancia de la aplicación sirve a múltiples clientes (tenants), manteniendo sus datos completamente aislados y seguros. En nuestro caso, cada "company" en InfraUX es un tenant independiente.
El Gran Debate: MongoDB vs PostgreSQL
Inicialmente, consideramos MongoDB por su flexibilidad con esquemas dinámicos. Los diagramas de infraestructura pueden ser muy variados, y la naturaleza schema-less de MongoDB parecía ideal. Sin embargo, después de semanas de análisis, PostgreSQL emergió como el ganador.
Por qué elegimos PostgreSQL:
Característica | PostgreSQL | MongoDB |
---|---|---|
Row Level Security | ✅ Nativo | ❌ Manual |
JSONB | ✅ Flexibilidad + Estructura | ✅ Solo flexibilidad |
Transacciones ACID | ✅ Completas | ⚠️ Limitadas |
Integración Supabase | ✅ Perfecta | ❌ No disponible |
Madurez | ✅ 30+ años | ✅ 10+ años |
Implementación de Row Level Security
RLS es la piedra angular de nuestra arquitectura multi-tenant. Cada tabla tiene políticas que garantizan que los usuarios solo pueden acceder a datos de su company:
1-- Política para la tabla diagrams
2CREATE POLICY "Users can only see their company diagrams" ON diagrams
3 FOR SELECT
4 USING (company_id = auth.jwt() ->> 'company_id');
5
6-- Política para INSERT
7CREATE POLICY "Users can only create diagrams in their company" ON diagrams
8 FOR INSERT
9 WITH CHECK (company_id = auth.jwt() ->> 'company_id');
10
11-- Política para UPDATE
12CREATE POLICY "Users can only update their company diagrams" ON diagrams
13 FOR UPDATE
14 USING (company_id = auth.jwt() ->> 'company_id')
15 WITH CHECK (company_id = auth.jwt() ->> 'company_id');
Estructura de Datos Multi-tenant
Nuestra estructura de base de datos sigue un patrón consistente:
1-- Tabla companies (root del tenant)
2CREATE TABLE companies (
3 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4 name TEXT NOT NULL,
5 slug TEXT UNIQUE NOT NULL,
6 created_at TIMESTAMPTZ DEFAULT NOW(),
7 settings JSONB DEFAULT '{}'::jsonb
8);
9
10-- Tabla users con relación a company
11CREATE TABLE users (
12 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
13 company_id UUID REFERENCES companies(id) ON DELETE CASCADE,
14 email TEXT UNIQUE NOT NULL,
15 role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
16 created_at TIMESTAMPTZ DEFAULT NOW()
17);
18
19-- Tabla diagrams con company_id
20CREATE TABLE diagrams (
21 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
22 company_id UUID REFERENCES companies(id) ON DELETE CASCADE,
23 name TEXT NOT NULL,
24 content JSONB NOT NULL, -- Aquí almacenamos el diagrama completo
25 environment TEXT NOT NULL,
26 created_by UUID REFERENCES users(id),
27 created_at TIMESTAMPTZ DEFAULT NOW(),
28 updated_at TIMESTAMPTZ DEFAULT NOW()
29);
30
31-- Índices para performance
32CREATE INDEX idx_diagrams_company_id ON diagrams(company_id);
33CREATE INDEX idx_diagrams_environment ON diagrams(company_id, environment);
34CREATE INDEX idx_diagrams_content_gin ON diagrams USING gin(content);
Gestión de Sesiones y Context
Cada request incluye el company_id en el JWT token, lo que permite a PostgreSQL aplicar automáticamente las políticas RLS:
1// middleware/auth.ts
2export async function withAuth(req: Request) {
3 const token = req.headers.get('Authorization')?.replace('Bearer ', '');
4
5 if (!token) {
6 throw new Error('No authentication token');
7 }
8
9 const { data: { user }, error } = await supabase.auth.getUser(token);
10
11 if (error || !user) {
12 throw new Error('Invalid token');
13 }
14
15 // El company_id está en los metadatos del usuario
16 const companyId = user.user_metadata.company_id;
17
18 // Supabase automáticamente incluye esto en el JWT
19 return { user, companyId };
20}
Beneficios de Nuestra Arquitectura
<div class="success-box"> ✅ **Seguridad por Diseño:** Con RLS, es imposible acceder accidentalmente a datos de otro tenant. Incluso si hay un bug en el código de la aplicación, PostgreSQL bloqueará el acceso. </div> <div class="info-box"> 💡 **Escalabilidad Horizontal:** Podemos escalar agregando más servidores de aplicación sin preocuparnos por la segregación de datos. </div> <div class="warning-box"> ⚠️ **Backup y Restore Granular:** Podemos hacer backup o restaurar datos de un tenant específico sin afectar a otros. </div>Desafíos y Soluciones
Desafío 1: Migraciones de Schema
Las migraciones deben ser cuidadosamente diseñadas para no romper el aislamiento de datos.
Solución: Todas las migraciones pasan por un proceso de revisión estricto y se prueban en un ambiente multi-tenant de staging.
Desafío 2: Performance con Muchos Tenants
A medida que crecemos, las queries pueden volverse lentas si no están bien optimizadas.
Solución: Índices estratégicos en company_id y particionamiento de tablas grandes por tenant.
Desafío 3: Límites y Quotas por Tenant
Diferentes planes necesitan diferentes límites de recursos.
Solución: Sistema de quotas flexible almacenado en la tabla companies:
1{
2 "quotas": {
3 "max_diagrams": 10,
4 "max_users": 5,
5 "max_environments": 3,
6 "features": {
7 "collaboration": true,
8 "api_access": false,
9 "custom_domains": false
10 }
11 }
12}
Lecciones Aprendidas
- Invertir en arquitectura temprano paga dividendos: La complejidad inicial se ha traducido en facilidad de mantenimiento
- RLS es poderoso pero requiere disciplina: Cada tabla nueva necesita políticas bien pensadas
- Testing multi-tenant es crítico: Tenemos tests específicos que verifican el aislamiento de datos
- Monitoreo por tenant es esencial: Necesitas saber qué tenant está causando problemas
El Futuro: Sharding y Más Allá
Aunque nuestra arquitectura actual puede manejar miles de tenants, ya estamos planeando el siguiente nivel:
- Sharding por tenant: Los tenants grandes podrán tener su propia base de datos
- Multi-región: Tenants podrán elegir dónde almacenar sus datos
- Bring Your Own Database: Empresas podrán usar su propia instancia de PostgreSQL
La decisión de ir multi-tenant desde el día 1 ha sido fundamental para nuestro éxito. Aunque agregó complejidad inicial, nos ha permitido crecer de manera segura y escalable, manteniendo los datos de nuestros clientes completamente aislados y protegidos.
Métricas de Performance
Métrica | Valor | Objetivo |
---|---|---|
Queries/segundo | 12,000 | 10,000 |
Latencia p50 | 23ms | < 50ms |
Latencia p99 | 87ms | < 200ms |
Aislamiento de datos | 100% | 100% |
Uptime | 99.97% | 99.9% |