- Prefijo v2: MCH - TRACEABILITY-MASTER.yml creado - Listo para integracion como submodulo Workspace: v2.0.0 | SIMCO: v4.0.0
198 lines
4.5 KiB
Markdown
198 lines
4.5 KiB
Markdown
---
|
|
id: ADR-0001
|
|
type: ADR
|
|
title: "Arquitectura Multi-Tenant"
|
|
status: Accepted
|
|
decision_date: 2026-01-04
|
|
updated_at: 2026-01-10
|
|
simco_version: "3.8.0"
|
|
stakeholders:
|
|
- "Equipo MiChangarrito"
|
|
tags:
|
|
- multi-tenant
|
|
- postgresql
|
|
- rls
|
|
- database
|
|
- arquitectura
|
|
- seguridad
|
|
---
|
|
|
|
# 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.
|
|
|
|
```sql
|
|
-- 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
|
|
|
|
1. **Escalabilidad:** Una sola base de datos maneja todos los tenants
|
|
2. **Simplicidad:** Migraciones aplican a todos
|
|
3. **Eficiencia:** Pool de conexiones compartido
|
|
4. **Seguridad:** RLS es enforced a nivel de DB
|
|
|
|
### Negativas
|
|
|
|
1. **Disciplina:** Todas las queries deben considerar tenant_id
|
|
2. **Testing:** Necesitamos tests de aislamiento
|
|
3. **Performance:** Indices deben incluir tenant_id
|
|
|
|
### Neutrales
|
|
|
|
1. **Backup:** Todos los tenants juntos
|
|
2. **Monitoreo:** Una base de datos que monitorear
|
|
|
|
---
|
|
|
|
## Implementacion
|
|
|
|
### Estructura de Tablas
|
|
|
|
```sql
|
|
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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
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
|
|
|
|
- [PostgreSQL RLS Documentation](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)
|
|
- [Multi-tenant SaaS patterns](https://docs.microsoft.com/en-us/azure/sql-database/sql-database-design-patterns-multi-tenancy)
|
|
- [DATABASE_INVENTORY.yml](../../orchestration/inventarios/DATABASE_INVENTORY.yml)
|
|
|
|
---
|
|
|
|
**Fecha decision:** 2026-01-06
|
|
**Autores:** Architecture Team
|