template-saas/docs/97-adr/adr/ADR-001-multi-tenancy-rls.md
Adrian Flores Cortes 806612a4db
Some checks are pending
CI / Backend CI (push) Waiting to run
CI / Frontend CI (push) Waiting to run
CI / Security Scan (push) Waiting to run
CI / CI Summary (push) Blocked by required conditions
[REESTRUCTURA-DOCS] refactor: Corregir estructura docs/ segun SIMCO-DOCUMENTACION-PROYECTO
- 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>
2026-01-24 20:34:14 -06:00

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)