michangarrito/docs/97-adr/ADR-0001-multi-tenant-architecture.md
rckrdmrd 928eb795e6 [SIMCO-V38] feat: Actualizar a SIMCO v3.8.0 + cambios apps
- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8
- Cambios en backend y frontend

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 08:53:05 -06:00

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