- Prefijo v2: MCH - TRACEABILITY-MASTER.yml creado - Listo para integracion como submodulo Workspace: v2.0.0 | SIMCO: v4.0.0
4.5 KiB
4.5 KiB
| id | type | title | status | decision_date | updated_at | simco_version | stakeholders | tags | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ADR-0001 | ADR | Arquitectura Multi-Tenant | Accepted | 2026-01-04 | 2026-01-10 | 3.8.0 |
|
|
ADR-0001: Arquitectura Multi-Tenant
Metadata
| Campo | Valor |
|---|---|
| ID | ADR-0001 |
| Estado | Accepted |
| Fecha | 2026-01-06 |
| Autor | Architecture Team |
| Supersede | - |
Contexto
MiChangarrito es un SaaS para multiples tiendas independientes. Cada tienda (tenant) debe tener sus datos completamente aislados de otras tiendas por razones de:
- Seguridad: Un tenant no puede ver datos de otro
- Privacidad: Informacion financiera sensible
- Regulacion: Cumplimiento con leyes de proteccion de datos
- Escalabilidad: Soporte para miles de tiendas
Se necesita decidir la estrategia de multi-tenancy para la base de datos.
Decision
Adoptamos el modelo de multi-tenancy por columna (tenant_id) con Row Level Security (RLS) de PostgreSQL.
Cada tabla tiene una columna tenant_id y politicas RLS que filtran automaticamente los datos segun el tenant actual.
-- Ejemplo de RLS
ALTER TABLE catalog.products ENABLE ROW LEVEL SECURITY;
CREATE POLICY products_tenant_isolation ON catalog.products
USING (tenant_id = current_setting('app.current_tenant')::UUID);
Alternativas Consideradas
Opcion 1: Base de datos por tenant
- Pros:
- Aislamiento total
- Facil backup/restore individual
- Sin riesgo de data leaks
- Cons:
- Costoso en recursos
- Complejo de mantener (miles de DBs)
- Migraciones complicadas
Opcion 2: Schema por tenant
- Pros:
- Buen aislamiento
- Un solo servidor
- Backup conjunto
- Cons:
- Limite de schemas en PostgreSQL
- Migraciones complicadas
- Connection pooling complejo
Opcion 3: Tenant ID con RLS (Elegida)
- Pros:
- Simple de implementar
- Escalable
- Migraciones faciles
- RLS garantiza aislamiento
- Eficiente en recursos
- Cons:
- Requiere disciplina (no olvidar tenant_id)
- Dependencia de RLS
- Query planning puede ser afectado
Consecuencias
Positivas
- Escalabilidad: Una sola base de datos maneja todos los tenants
- Simplicidad: Migraciones aplican a todos
- Eficiencia: Pool de conexiones compartido
- Seguridad: RLS es enforced a nivel de DB
Negativas
- Disciplina: Todas las queries deben considerar tenant_id
- Testing: Necesitamos tests de aislamiento
- Performance: Indices deben incluir tenant_id
Neutrales
- Backup: Todos los tenants juntos
- Monitoreo: Una base de datos que monitorear
Implementacion
Estructura de Tablas
CREATE TABLE catalog.products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
name VARCHAR(255) NOT NULL,
price DECIMAL(10,2),
-- ...
CONSTRAINT idx_products_tenant UNIQUE (tenant_id, id)
);
Middleware de Contexto
// Interceptor NestJS
@Injectable()
export class TenantInterceptor implements NestInterceptor {
async intercept(context: ExecutionContext, next: CallHandler) {
const request = context.switchToHttp().getRequest();
const tenantId = request.user?.tenantId;
// Setear tenant en conexion
await this.dataSource.query(
`SET app.current_tenant = '${tenantId}'`
);
return next.handle();
}
}
Validacion
Criterios de Exito
- Todas las tablas tienen tenant_id
- RLS habilitado en todas las tablas
- Tests de aislamiento pasan
- No hay queries sin filtro de tenant
Tests de Aislamiento
it('should not allow tenant A to see tenant B data', async () => {
// Setup tenant A
await setTenant(tenantAId);
const productA = await createProduct({ name: 'Product A' });
// Switch to tenant B
await setTenant(tenantBId);
const products = await productService.findAll();
// Tenant B should not see product A
expect(products).not.toContainEqual(
expect.objectContaining({ id: productA.id })
);
});
Referencias
Fecha decision: 2026-01-06 Autores: Architecture Team