- Renombrar 02-integraciones/ → 03-integraciones/ (resolver prefijo duplicado) - Renombrar 02-devops/ → 04-devops/ (resolver prefijo duplicado) - Renombrar architecture/ → 97-adr/ (agregar prefijo numerico) - Actualizar _MAP.md con nueva estructura y version 2.1.0 Estructura final: - 00-vision-general/ - 01-modulos/ - 02-especificaciones/ - 03-integraciones/ - 04-devops/ - 97-adr/ Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
163 lines
4.2 KiB
Markdown
163 lines
4.2 KiB
Markdown
---
|
|
id: "ADR-001"
|
|
title: "Multi-tenancy con PostgreSQL Row-Level Security"
|
|
type: "ADR"
|
|
status: "Accepted"
|
|
priority: "P0"
|
|
supersedes: "N/A"
|
|
superseded_by: "N/A"
|
|
version: "1.0.0"
|
|
created_date: "2026-01-07"
|
|
updated_date: "2026-01-10"
|
|
---
|
|
|
|
# ADR-001: Multi-tenancy con PostgreSQL Row-Level Security
|
|
|
|
## Metadata
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| ID | ADR-001 |
|
|
| Estado | Accepted |
|
|
| Fecha | 2026-01-10 |
|
|
| Supersede | N/A |
|
|
|
|
## Contexto
|
|
|
|
Template SaaS requiere soporte para múltiples organizaciones (tenants) en una misma instancia de la aplicación. Cada tenant debe tener sus datos completamente aislados de otros tenants por razones de seguridad y privacidad.
|
|
|
|
### Requisitos
|
|
- Aislamiento completo de datos entre tenants
|
|
- Escalabilidad para miles de tenants
|
|
- Simplicidad operacional (un solo deployment)
|
|
- Performance consistente
|
|
- Facilidad de backup/restore por tenant
|
|
|
|
## Opciones Consideradas
|
|
|
|
### Opción 1: Database per Tenant
|
|
**Descripción:** Cada tenant tiene su propia base de datos PostgreSQL.
|
|
|
|
**Pros:**
|
|
- Aislamiento total de datos
|
|
- Fácil backup/restore individual
|
|
- Sin riesgo de data leaks entre tenants
|
|
|
|
**Contras:**
|
|
- Complejidad operacional alta (miles de DBs)
|
|
- Connection pooling complejo
|
|
- Migraciones costosas (aplicar a cada DB)
|
|
- Mayor costo de infraestructura
|
|
|
|
### Opción 2: Schema per Tenant
|
|
**Descripción:** Cada tenant tiene su propio schema dentro de una base de datos.
|
|
|
|
**Pros:**
|
|
- Mejor aislamiento que shared tables
|
|
- Backup/restore por schema posible
|
|
- Un solo connection pool
|
|
|
|
**Contras:**
|
|
- Migraciones aún costosas
|
|
- Límite práctico de schemas (~10,000)
|
|
- Complejidad en queries cross-tenant
|
|
|
|
### Opción 3: Row-Level Security (RLS) ✓
|
|
**Descripción:** Todos los tenants comparten las mismas tablas, con políticas RLS que filtran por `tenant_id`.
|
|
|
|
**Pros:**
|
|
- Operacionalmente simple
|
|
- Un solo set de tablas y migraciones
|
|
- Escalable a millones de tenants
|
|
- Performance optimizado con índices
|
|
- Queries simples y consistentes
|
|
|
|
**Contras:**
|
|
- Riesgo de data leaks si RLS mal configurado
|
|
- Requiere disciplina en desarrollo
|
|
- Backup individual más complejo
|
|
|
|
## Decisión
|
|
|
|
**Elegimos Row-Level Security (RLS)** por las siguientes razones:
|
|
|
|
1. **Simplicidad Operacional:** Un solo deployment, una base de datos, un set de migraciones
|
|
2. **Escalabilidad:** Soporta desde 1 hasta millones de tenants sin cambios arquitectónicos
|
|
3. **Performance:** PostgreSQL RLS es eficiente y se beneficia de índices
|
|
4. **Costo:** Menor infraestructura requerida
|
|
|
|
## Implementación
|
|
|
|
### Estructura de Tablas
|
|
|
|
Todas las tablas multi-tenant incluyen:
|
|
```sql
|
|
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id)
|
|
```
|
|
|
|
### Políticas RLS
|
|
|
|
```sql
|
|
-- Ejemplo para tabla users
|
|
ALTER TABLE users.users ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY users_tenant_isolation ON users.users
|
|
USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
|
|
```
|
|
|
|
### Context Setting
|
|
|
|
El `tenant_id` se establece en cada request:
|
|
```sql
|
|
SET app.current_tenant_id = 'uuid-del-tenant';
|
|
```
|
|
|
|
### Middleware NestJS
|
|
|
|
```typescript
|
|
@Injectable()
|
|
export class TenantContextInterceptor implements NestInterceptor {
|
|
async intercept(context: ExecutionContext, next: CallHandler) {
|
|
const request = context.switchToHttp().getRequest();
|
|
const tenantId = request.user?.tenant_id;
|
|
|
|
await this.dataSource.query(
|
|
`SET app.current_tenant_id = '${tenantId}'`
|
|
);
|
|
|
|
return next.handle();
|
|
}
|
|
}
|
|
```
|
|
|
|
## Consecuencias
|
|
|
|
### Positivas
|
|
- Código más simple y mantenible
|
|
- Un solo esquema de datos
|
|
- Migraciones atómicas
|
|
- Queries sin WHERE tenant_id manual
|
|
- Escalabilidad lineal
|
|
|
|
### Negativas
|
|
- Requiere testing exhaustivo de RLS
|
|
- Datos de todos los tenants en mismas tablas
|
|
- Backup por tenant requiere queries filtradas
|
|
- Posible contención en tablas muy grandes
|
|
|
|
### Mitigaciones
|
|
- Tests automatizados de aislamiento
|
|
- Auditoría de queries en desarrollo
|
|
- Particionamiento por tenant_id si necesario
|
|
- Monitoreo de performance por tenant
|
|
|
|
## Referencias
|
|
|
|
- [PostgreSQL RLS Documentation](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)
|
|
- [Multi-tenancy Patterns](https://docs.microsoft.com/en-us/azure/architecture/patterns/multi-tenant)
|
|
- Schema DDL: `apps/database/ddl/schemas/`
|
|
|
|
---
|
|
|
|
**Fecha decision:** 2026-01-10
|
|
**Autores:** Claude Code (Arquitectura)
|