Construí la arquitectura multi-tenant el Día 2. El Día 67, la reconstruí
Pensé que agregar org_id a cada tabla significaba una arquitectura multi-tenant a prueba de balas. Luego mi auditoría de seguridad reveló que las agencias estaban escribiendo en las tablas de PYMEs—no por un bug, sino por diseño.
27 de octubre de 2025, 11:47 PM. Estoy ejecutando lo que pensé sería una auditoría de seguridad rutinaria en STRAŦUM. Todo ha estado funcionando bien durante semanas—las PYMEs tienen sus datos, las agencias tienen los suyos, la arquitectura multi-tenant es sólida.
El café está frío. El script de auditoría está procesando los logs. Entonces lo veo: Las agencias estaban escribiendo en las tablas de PYMEs.
No a través de un bug. No a través de un agujero de seguridad. A través de la arquitectura misma.
Hace dos meses, tomé la decisión de construir arquitectura multi-tenant desde el Día 2. Un movimiento audaz para un fundador en solitario con un agente de IA funcionando. Agregué `org_id` a cada tabla, escribí políticas RLS, construí enrutamiento separado para PYMEs y Agencias. Estaba funcionando—las PYMEs tenían sus campañas, las agencias tenían sus clientes, los datos fluían a los lugares correctos.
O eso creía.
Estuve ahí sentado durante probablemente 20 minutos mirando fijamente el esquema. ¿Cómo me perdí esto? Había pasado semanas construyendo arquitectura multi-tenant, escribiendo 83 políticas RLS, probando con cuentas tanto de PYME como de Agencia. Todo *funcionaba*. Pero "funcionar" y "ser correcto" no son lo mismo.
Este es el tipo de bug que te hace cuestionar si deberías estar construyendo software en absoluto. Porque no es un error tipográfico. No es un caso límite olvidado. Es ingenuidad arquitectónica.
Había cometido el error clásico: asumí que el filtrado por `org_id` era suficiente para el aislamiento multi-tenant. No lo era.
Esta es la historia de descubrir que el verdadero aislamiento multi-tenant requiere más que solo agregar `org_id` a cada tabla—y las 33 migraciones en 48 horas que finalmente lo resolvieron.
---
> **Nota**: Los ejemplos SQL en este artículo usan nombres de esquema y tabla genéricos (`tenant_b`, `workspace_entities`, `entity_data`) por seguridad. Los conceptos son los mismos independientemente de tus convenciones de nomenclatura específicas.
---
El problema: No todos los tenants son iguales
Esto es lo que inicialmente construí:
```sql
-- Brand guidelines table (shared by SMEs and Agencies)
CREATE TABLE brand_guidelines (
id UUID PRIMARY KEY,
org_id UUID REFERENCES organizations(id),
name TEXT,
guidelines JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- RLS policy (seems safe)
CREATE POLICY brand_guidelines_org_isolation ON brand_guidelines
FOR ALL TO authenticated
USING (org_id = get_user_org_id());
```
Esto funciona perfectamente para las PYMEs. Cada organización tiene sus propias directrices de marca. Row-Level Security garantiza que no puedan ver los datos de las demás. (Al menos, eso creía. Más sobre eso en un momento.)
Pero las Agencias son diferentes.
Las agencias no tienen solo un conjunto de directrices de marca. Tienen una por cliente:
- Directrices de marca del Cliente A (paleta de colores vibrante, tipografía en negrita, mensajes centrados en la innovación)
- Directrices de marca del Cliente B (paleta de colores apagada, diseño minimalista, posicionamiento centrado en la calidad)
Misma agencia, clientes diferentes, marcas completamente diferentes.
La solución ingenua (lo que construí primero):
```sql
-- Add entity_id to the shared table
ALTER TABLE brand_guidelines ADD COLUMN entity_id UUID;
-- Update RLS policy
CREATE POLICY brand_guidelines_isolation ON brand_guidelines
FOR ALL TO authenticated
USING (
organization_id = get_user_org_id() AND
(entity_id IS NULL OR entity_id = get_user_entity_id())
);
```
Problema: Esto creó una tabla con dos modelos de datos diferentes:
```
SME row: organization_id='org-123', entity_id=NULL, guidelines=\{...\}
Agency row: organization_id='org-456', entity_id='entity-a', guidelines=\{...\}
Agency row: organization_id='org-456', entity_id='entity-b', guidelines=\{...\}
```
Las consultas se convirtieron en un desastre con manejo complejo de `NULL`, y cada funcionalidad necesitaba lógica "si PYME, si no Agencia" en el código de la aplicación.
Y sí, escribí todo esto antes de darme cuenta de que eran síntomas de un problema más profundo. Semanas de trabajo, todas apuntando a la misma conclusión: me había arquitectado en un callejón sin salida.
Cada funcionalidad necesitaba lógica personalizada: "Si PYME, haz esto. Si Agencia, haz aquello."
Peor aún, la arquitectura hacía suposiciones incorrectas:
- Las agencias escribiendo `entity_id=NULL` contaminarían los datos de PYMEs
- Las PYMEs no podían tener sub-entidades incluso si querían sub-cuentas
- El esquema se volvió "queso suizo" con columnas anulables
Esto no era arquitectura multi-tenant. Era una tabla única intentando servir dos modelos de datos diferentes.
---
La revelación: Los diferentes tenants necesitan esquemas diferentes
A finales de octubre, me di cuenta de la verdad: Las PYMEs y las Agencias no comparten el mismo modelo de datos.
Modelo de datos de PYME:
```
organization → campaigns → agent_outputs
```
Modelo de datos de Agencia:
```
organization → workspace_entities → campaigns → agent_outputs
↓
entity_data (e.g., brand guidelines, personas)
```
Las agencias tienen una capa completa (workspace entities) que las PYMEs no tienen. También tienen inteligencia específica de entidad que no debería existir en el mundo de las PYMEs.
La solución: Esquemas de base de datos separados.
```sql
-- SME tables (public schema)
public.brand_guidelines
public.campaigns
public.outputs
-- Agency tables (tenant_b schema)
tenant_b.workspace_entities
tenant_b.entity_data -- Includes brand guidelines, personas, etc.
tenant_b.campaigns
tenant_b.outputs
```
Ahora las PYMEs y las Agencias tienen tablas completamente diferentes. Sin esquema compartido. Sin contaminación de `entity_id` anulable. Sin lógica "si PYME, si no Agencia".
---
Por qué importa: El caso de negocio para el enrutamiento de esquemas
Antes de profundizar en la implementación técnica, hablemos de por qué esta decisión arquitectónica importa más allá de "es código más limpio."
Preparación para el crecimiento (quizás)
El enrutamiento de esquemas no se trata solo de resolver el problema de hoy. Se trata de mantener puertas abiertas para oportunidades que ni siquiera puedo predecir todavía.
Todavía estoy en alpha privado con 15 usuarios. No tengo clientes empresariales. No he hablado con un abogado de GDPR. Pero esto es lo que el enrutamiento de esquemas *podría* habilitar si STRAŦUM crece:
Expansión internacional:
- Si expandimos a la UE: Los esquemas separados podrían habilitar la residencia de datos (datos de clientes de la UE en el esquema `eu_agency` en servidores de la UE)
- El derecho a la eliminación se vuelve más simple: Consulta un esquema, no filtres a través de tablas mixtas
- Pistas de auditoría: "Muéstrame todos los datos del Cliente X" = una consulta de esquema
Conversaciones de cumplimiento:
- Cuando alguien finalmente pregunte "¿Cómo garantizas el aislamiento de datos?"
- Con filtrado por `org_id`: "Usamos políticas de Row-Level Security" (vago, difícil de verificar)
- Con enrutamiento de esquemas: "Los datos de cada cliente viven en un esquema de base de datos separado" (concreto, auditable)
- No sé si esto importa todavía. Pero *podría* importar si tenemos esas conversaciones.
La verdad honesta:
No estoy construyendo para cumplimiento HIPAA o SOC 2 ahora mismo. Estoy construyendo para PYMEs y pequeñas agencias que necesitan mejor estrategia de marketing.
Pero el enrutamiento de esquemas significa que si alguien pregunta "¿Pueden manejar clientes de salud?" o "¿Soportan la residencia de datos?" algún día, la respuesta es "sí, déjame mostrarte la arquitectura" en lugar de "déjame reconstruir todo primero."
Las desventajas (siendo honesto)
El enrutamiento de esquemas no tiene solo ventajas. Aquí está lo que realmente cuesta:
Complejidad de desarrollo:
- Cada operación WRITE necesita una función de enrutador
- Cada operación READ necesita una vista de seguridad
- Las pruebas requieren rutas tanto de PYME como de Agencia
- Con Claude Code: 2 días de trabajo intenso (27-29 oct, 2025) por las tardes
- Sin herramientas de IA: Habrían sido semanas
Riesgo de migración:
- 33 migraciones secuenciales = 33 oportunidades para errores tipográficos
- Un `ALTER TABLE` incorrecto = corrupción de datos en producción
- Tuve que ejecutar cada migración 3 veces en staging antes de tocar producción
- La paranoia era real
Sobrecarga de rendimiento de consultas:
- Vistas con `UNION ALL` = lecturas ligeramente más lentas
- Funciones de enrutador = llamada de función extra en escrituras
- RLS + vistas = planes de consulta más complejos
- (En la práctica: Todavía no he notado ralentizaciones, pero también solo tengo 15 usuarios alpha)
Complejidad operacional:
- Las migraciones de esquema ahora afectan a 2+ esquemas (public + agency)
- Las copias de seguridad de base de datos necesitan restauración consciente del esquema
- Las consultas de monitoreo necesitan verificar múltiples esquemas
- Esto me va a afectar eventualmente, solo no sé cuándo
Por qué hice el compromiso de todas formas
El valor de opción podría ser enorme. O podría no importar en absoluto.
El enrutamiento de esquemas mantiene puertas abiertas que ni siquiera estoy seguro de querer cruzar:
- Asociaciones de marca blanca: Podría dar a un socio su propio esquema, rebrandear la UI
- Oportunidades de revendedor: Las agencias podrían revender con aislamiento de datos demostrable
- Diferentes niveles de precios: Los clientes "Premium" podrían obtener esquemas dedicados
- Expansión geográfica: Esquema de la UE, esquema de EE.UU., esquema de APAC - mismo codebase
La realidad es que estoy en alpha privado. No sé si alguno de estos importará. Tal vez nunca reciba una solicitud de marca blanca. Tal vez la expansión geográfica sea a años de distancia. Tal vez todo el negocio pivote y nada de esto sea relevante.
Pero esto es lo que sí sé: con el enrutamiento de esquemas, estas opciones existen. Con el filtrado por `org_id`, la mayoría de ellas requerirían una reescritura completa.
Esa es la apuesta que hice: Gastar 2 días extra ahora (con Claude Code) para mantener opciones abiertas después.
¿Es la apuesta correcta? Pregúntame en un año.
---
La arquitectura: Enrutamiento de esquemas
Patrón 1: Tablas específicas de esquema
Algunas tablas solo existen para un tipo de tenant:
```sql
-- Specialized tenant schema
CREATE SCHEMA tenant_b;
-- Workspace entities (specific to this tenant type)
CREATE TABLE tenant_b.workspace_entities (
id UUID PRIMARY KEY,
organization_id UUID,
name TEXT,
metadata JSONB
);
-- Entity-specific data
CREATE TABLE tenant_b.entity_data (
id UUID PRIMARY KEY,
organization_id UUID,
entity_id UUID REFERENCES tenant_b.workspace_entities(id),
data_type TEXT,
content JSONB
);
```
Las PYMEs nunca tocan estas tablas. No existen en el esquema `public`.
Patrón 2: Funciones de enrutador de base de datos
¿Cómo escribes en el esquema correcto? **Funciones de enrutador**.
Aquí está el concepto (simplificado):
```sql
CREATE FUNCTION save_resource_routed(params)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER -- Run with elevated privileges
AS $$
BEGIN
-- Step 1: Detect organization type
SELECT type INTO org_type FROM organizations WHERE id = p_org_id;
-- Step 2: Route to correct schema based on type
IF org_type = 'TENANT_B' THEN
INSERT INTO tenant_b.entity_data (...) VALUES (...);
ELSE
INSERT INTO public.brand_guidelines (...) VALUES (...);
END IF;
RETURN result;
END;
$$;
```
Cómo funciona:
1. Detectar tipo de organización: Consultar la tabla de organizaciones para determinar el tipo de tenant
2. Enrutar al esquema correcto: Escribir en el esquema apropiado según el tipo
3. Devolver resultado: Incluir qué esquema se usó para depuración
Código de la aplicación (igual para todos los tipos de tenant):
```typescript
// Just call the router function - no tenant-specific logic
const result = await supabase.rpc('save_resource_routed', \{
p_org_id: orgId,
p_entity_id: entityId, // null for simple tenants
p_data: { ... \}
});
```
Sin if/else en el código de la aplicación. La base de datos hace el enrutamiento.
Escribir mi primera función de enrutador tomó 4 horas. ¿Depurar por qué no funcionaba? Otras 6 horas. ¿El problema? Me había olvidado de otorgar permisos de EXECUTE. Energía clásica de fundador en solitario: brillantez arquitectónica, descuidos en permisos. :P
Patrón 3: Vistas de seguridad-invocador para lecturas
Las escrituras usan funciones de enrutador. Las lecturas usan **vistas**.
```sql
-- Unified view combining both schemas
CREATE VIEW resources_unified
WITH (security_invoker = on) -- Respects RLS policies
AS
SELECT id, organization_id, NULL AS entity_id, data, 'public' AS source
FROM public.brand_guidelines
UNION ALL
SELECT id, organization_id, entity_id, content AS data, 'tenant_b' AS source
FROM tenant_b.entity_data
WHERE data_type = 'brand_guidelines';
```
Código de la aplicación (lecturas unificadas):
```typescript
// Read resources (works for all tenant types)
const \{ data \} = await supabase
.from('resources_unified')
.select('*')
.eq('organization_id', orgId);
// RLS policies filter correctly regardless of source schema
```
Detalle clave: `WITH (security_invoker = on)` garantiza que se apliquen las políticas RLS. Sin esto, las vistas evitan RLS (desastre de seguridad).
---
La migración: 33 migraciones en 48 horas
Agregar enrutamiento de esquemas no fue una migración única. Fue un viaje.
¿Sabes qué es divertido? Escribir 33 migraciones de base de datos seguidas sabiendo que si UNA tiene un error tipográfico, corromperás los datos de producción. En realidad, "divertido" no es la palabra. "Aterrador" es más preciso. Ejecuté cada migración en staging tres veces antes de tocar producción.
27-29 de octubre de 2025: 33 migraciones secuenciales para enrutamiento de esquemas completo.
Las fases de migración:
1. Crear esquema especializado - Configurar el esquema `tenant_b` con los permisos adecuados
2. Crear tablas específicas del esquema - Duplicar las tablas necesarias en el nuevo esquema
3. Construir funciones de enrutador - Una para cada tipo de recurso que necesita enrutamiento
4. Crear vistas de seguridad - Vistas unificadas con `UNION ALL` para lecturas
5. Actualizar políticas RLS - Garantizar que ambos esquemas tengan el aislamiento adecuado
6. Migración de datos - Mover los datos existentes a los esquemas correctos
7. Actualizaciones de la aplicación - Cambiar de consultas directas a funciones de enrutador/vistas
Esfuerzo total: 33 migraciones, 2 días con Claude Code, 100% valió la pena.
---
Los resultados: Verdadero aislamiento multi-tenant
Antes (tablas compartidas con org_id)
Modelo de datos:
```sql
public.brand_guidelines (organization_id, entity_id, guidelines)
```
Problemas:
- ❌ `entity_id` anulable para un tipo de tenant (confusión en el modelo de datos)
- ❌ Consultas complejas con manejo de `NULL`
- ❌ Lógica de la aplicación: `if (tenantTypeA) { ... } else { ... }`
- ❌ Riesgo de contaminación cruzada
Después (enrutamiento de esquemas)
Modelo de datos:
```sql
public.brand_guidelines (organization_id, guidelines) -- Tenant Type A
tenant_b.entity_data (organization_id, entity_id, data) -- Tenant Type B
```
Beneficios:
- ✅ Modelos de datos limpios (sin claves foráneas anulables)
- ✅ Consultas simples sin manejo complejo de `NULL`
- ✅ Sin if/else en la aplicación (la base de datos maneja el enrutamiento)
- ✅ Imposible la contaminación entre esquemas (físicamente separados)
Mejoras de seguridad
Antes: Riesgo de contaminación cruzada con columnas anulables y tablas compartidas
Después: Las funciones de enrutador dirigen automáticamente las escrituras al esquema correcto según el tipo de organización. La separación física de esquemas hace imposible la contaminación cruzada.
Nivel de aislamiento: Separación aplicada por la base de datos. No verificaciones a nivel de aplicación.
---
Cuándo usar enrutamiento de esquemas vs. filtrado por org_id
No todas las aplicaciones multi-tenant necesitan enrutamiento de esquemas. Usa este árbol de decisiones:
Usa filtrado por `org_id` (más simple) si:
✅ Todos los tenants tienen el mismo modelo de datos (p.ej., aplicación de tareas)
✅ Sin tenencia jerárquica (sin sub-entidades dentro de organizaciones)
✅ Consultas simples (`WHERE org_id = X` funciona en todas partes)
✅ B2C o pequeño B2B (sin ventas empresariales, sin requisitos de cumplimiento)
✅ La velocidad del MVP importa (llega al mercado en semanas, no meses)
Razonamiento de negocio: Estás validando el product-market fit, no construyendo para los requisitos de cumplimiento de Fortune 500. Envía rápido, refactoriza después si obtienes tracción empresarial.
Ejemplo: Herramienta de gestión de proyectos donde cada organización gestiona sus propios proyectos.
```sql
CREATE TABLE projects (
id UUID PRIMARY KEY,
org_id UUID, -- Simple filtering
name TEXT
);
```
Usa enrutamiento de esquemas (más complejo) si:
✅ Los diferentes tipos de tenant necesitan diferentes modelos de datos (Tipo A vs. Tipo B vs. Tipo C)
✅ Tenencia jerárquica (organizaciones → workspace_entities → sub-entidades)
✅ Ventas empresariales en el roadmap (Fortune 500, salud, finanzas, gobierno)
✅ Cumplimiento regulatorio requerido (GDPR, HIPAA, SOC 2, FedRAMP)
✅ Potencial de marca blanca o revendedor (los socios necesitan aislamiento de datos completo)
✅ Expansión internacional planificada (requisitos de residencia de datos)
Razonamiento de negocio: Si estás apuntando a clientes empresariales, el "aislamiento de datos" se convierte en una casilla de verificación en los cuestionarios de seguridad. El enrutamiento de esquemas te permite responder con confianza. El filtrado a nivel de fila te deja vacilando.
Ejemplo: Plataforma donde algunos tenants tienen estructuras de workspace jerárquicas que difieren fundamentalmente de los clientes directos.
Costo/Beneficio (mi experiencia):
- Costos del enrutamiento de esquemas: 2 días con Claude Code (habrían sido semanas sin asistencia de IA)
- Posible ventaja: Arquitectura más limpia, preparación para cumplimiento, opciones de asociación
- Ventaja real: Desconocida - todavía estoy en alpha privado
- Punto de equilibrio: Si el enrutamiento de esquemas abre aunque sea una puerta que de otra manera no podría cruzar, se paga solo
La pregunta real: ¿Estás optimizando para velocidad de salida al mercado o para opcionalidad? Ambas son válidas. Yo elegí opcionalidad.
---
Estrategias alternativas de aislamiento
El enrutamiento de esquemas no es el único enfoque. Aquí está el espectro:
Nivel 1: Bases de datos separadas (mayor aislamiento)
```
database_tenant_1
database_tenant_2
database_tenant_3
```
Ventajas:
- ✅ Aislamiento físico completo
- ✅ Copias de seguridad por tenant
- ✅ Escalado independiente
- ✅ Cumplimiento regulatorio (residencia de datos)
Desventajas:
- ❌ Alta complejidad operacional (gestionar N bases de datos)
- ❌ Costoso (instancia de base de datos por tenant)
- ❌ Consultas entre tenants imposibles
- ❌ Migraciones de esquema en todas las bases de datos
Caso de uso: SaaS empresarial con requisitos regulatorios, clientes de alto valor ($10k+/mes).
Nivel 2: Esquemas separados (aislamiento fuerte)
```
database
├── schema_tenant_1
├── schema_tenant_2
└── public (shared tables)
```
Ventajas:
- ✅ Aislamiento lógico fuerte
- ✅ Infraestructura compartida (una base de datos)
- ✅ Permisos a nivel de esquema
- ✅ Diferentes modelos de datos por tipo de tenant
Desventajas:
- ❌ Más complejo que a nivel de fila
- ❌ Requiere funciones de enrutador
- ❌ Complejidad de migración (N esquemas)
Caso de uso: SaaS B2B con diferentes niveles de clientes (el enfoque de STRAŦUM).
Nivel 3: Filtrado a nivel de fila con RLS (aislamiento moderado)
```
database
└── public
└── table (with org_id column)
```
Ventajas:
- ✅ Simple de implementar
- ✅ Migraciones fáciles (un esquema)
- ✅ Analíticas entre tenants posibles
- ✅ RLS de Postgres aplica el aislamiento
Desventajas:
- ❌ Todos los tenants comparten el mismo modelo de datos
- ❌ Sobrecarga de rendimiento de RLS
- ❌ Riesgo de errores de configuración de RLS
Caso de uso: SaaS B2B con modelos de datos uniformes (gestión de proyectos, CRM).
---
Lista de verificación de implementación: Enrutamiento de esquemas
Si estás implementando enrutamiento de esquemas, usa esta lista de verificación:
Fase 1: Diseño de esquema
- [ ] Crear discriminador de tipo de tenant (`organizations.type`)
- [ ] Diseñar tablas específicas del esquema (¿qué pertenece a dónde?)
- [ ] Crear esquema especializado: `CREATE SCHEMA tenant_b;`
- [ ] Duplicar las tablas necesarias en el esquema especializado
- [ ] Documentar qué tablas viven en qué esquema
Fase 2: Funciones de enrutador
- [ ] Escribir función de enrutador para cada tipo de recurso
- [ ] Usar `SECURITY DEFINER` para permisos elevados
- [ ] Establecer `search_path = public, tenant_b` para acceso multi-esquema
- [ ] Manejar detección del tipo de tenant: `SELECT type FROM organizations`
- [ ] Devolver información del esquema para depuración
- [ ] Otorgar `EXECUTE` al rol autenticado
Fase 3: Vistas de seguridad
- [ ] Crear vistas unificadas para lecturas (`UNION ALL` entre esquemas)
- [ ] Usar `WITH (security_invoker = on)` para aplicación de RLS
- [ ] Agregar columna `source_schema` para depuración
- [ ] Probar que las políticas RLS funcionan en las vistas
- [ ] Otorgar `SELECT` al rol autenticado
Fase 4: Integración de la aplicación
- [ ] Actualizar escrituras para usar funciones de enrutador: `supabase.rpc('save_resource_routed', ...)`
- [ ] Actualizar lecturas para usar vistas: `supabase.from('resource_unified').select()`
- [ ] Eliminar lógica if/else a nivel de aplicación
- [ ] Probar flujo del tipo de tenant A (escrituras en `public`)
- [ ] Probar flujo del tipo de tenant B (escrituras en `tenant_b`)
- [ ] Verificar aislamiento de datos (Entidad A ≠ Entidad B)
Fase 5: Migración y pruebas
- [ ] Escribir scripts de migración (30+ para cobertura completa)
- [ ] Probar en staging con datos realistas
- [ ] Ejecutar auditoría de seguridad (¿filtración entre esquemas?)
- [ ] Prueba de carga (rendimiento de RLS + vistas)
- [ ] Monitorear en producción (¿consultas lentas?)
---
Lecciones aprendidas
1. org_id es necesario, no suficiente
Agregar `org_id` a cada tabla te da filtrado a nivel de fila. Pero si los diferentes tipos de tenant necesitan diferentes modelos de datos, necesitas enrutamiento de esquemas.
Aprendizaje: "Multi-tenant" no es binario. Hay niveles de aislamiento. Elegí un nivel más alto del que estrictamente necesitaba para mis actuales 15 usuarios. El tiempo dirá si eso fue inteligente o solo trabajo extra.
2. Lógica de la aplicación → Lógica de base de datos
Cada `if (tenantType === 'TYPE_B')` en tu aplicación es un code smell. Mueve la lógica consciente del tenant a la base de datos con funciones de enrutador.
Aprendizaje: Las funciones de base de datos son más difíciles de escribir pero potencialmente más fáciles de auditar. Si alguna vez tengo clientes empresariales preguntando "demuestra tu aislamiento de datos," puedo señalar los stored procedures. Pero ahora mismo, eso es hipotético.
3. Vistas + RLS = Lecturas unificadas
Leer desde múltiples esquemas es complejo. Las vistas + `security_invoker = on` te dan lecturas unificadas con el aislamiento adecuado.
Aprendizaje: Las vistas crean capas de abstracción que podrían hacer el cumplimiento más fácil algún día. O podrían simplemente agregar complejidad que no necesitaba. Ya veremos.
4. Las funciones Security Definer son poderosas
`SECURITY DEFINER` permite que las funciones se ejecuten con privilegios elevados respetando aún las políticas RLS. Esencial para las funciones de enrutador.
5. Las migraciones valen la pena (probablemente)
33 migraciones para el enrutamiento de esquemas parecían muchas. ¿Pero el resultado? Arquitectura limpia, verdadero aislamiento, y cero bugs de contaminación entre tenants.
Aprendizaje: La deuda técnica tiene un precio. Elegí pagarlo temprano cuando tengo 15 usuarios alpha en lugar de esperar hasta tener clientes que pagan. ¿Fue esa la decisión correcta? Lo sabré si alguna vez tengo 100 clientes de los que preocuparme.
6. Los fallos arquitectónicos duelen más que los bugs de código
¿Encontrar una excepción de puntero nulo a las 11:47 PM? Molesto. ¿Descubrir que toda tu arquitectura multi-tenant está fundamentalmente rota? Ese es el tipo de descubrimiento que te hace quedarte despierto por las noches.
Pero esto es lo que aprendí: los errores arquitectónicos son solucionables. Son costosos, sí. Consumen tiempo, absolutamente. Pero pasé de "un tipo de tenant puede accidentalmente contaminar los datos de otro" a "aislamiento aplicado por la base de datos que es imposible eludir."
Corrígelo pronto, corrígelo bien, y dormirás mejor.
7. La arquitectura podría ser estrategia (o quizás solo sobre-ingeniería)
La decisión de enrutamiento de esquemas no fue solo sobre "código limpio." Fue sobre mantener opciones futuras abiertas—asociaciones de marca blanca, ventas empresariales, expansión internacional.
Pero aquí está la verdad honesta: estoy en alpha privado con 15 usuarios. No tengo clientes empresariales llamando. No he recibido una sola pregunta sobre GDPR. Las asociaciones que imagino podrían nunca materializarse.
Pensé que estaba tomando una decisión técnica. Quizás estaba tomando una decisión de estrategia de negocio. O quizás simplemente estaba sobre-ingeniando porque encuentro la arquitectura de bases de datos interesante. :)
¿Alguna vez descubriste un fallo arquitectónico que no era un bug sino un error de diseño? ¿Cómo manejaste la reconstrucción—lo arreglaste incrementalmente o arrancaste todo de raíz como yo?
Un abrazo,
Chandler
La serie de arquitectura de STRAŦUM: Esta es la parte 2 del viaje multi-tenant. Comenzó con construir arquitectura multi-tenant el Día 2. Después de la reconstrucción del esquema, descubrí 31 pantallas en blanco por contexto de navegación perdido y que mi base de datos era correcta pero 296 veces demasiado lenta.
---
¿Construyendo SaaS multi-tenant con necesidades complejas de aislamiento? STRAŦUM usa enrutamiento de esquemas para servir diferentes tipos de tenants con verdadero aislamiento de datos. Solicita acceso alpha en https://stratum.chandlernguyen.com/request-invitation
---
*Todavía aprendiendo que "multi-tenant" tiene muchos niveles de aislamiento. Todavía depurando políticas RLS a medianoche. Todavía cuestionando mis decisiones de arquitectura del Día 2 (pero menos que antes). Más aventuras de base de datos en https://www.chandlernguyen.com/ .
---





