template-saas/docs/00-vision-general/ARQUITECTURA-MULTI-TENANT.md
rckrdmrd 50a821a415
Some checks failed
CI / Backend CI (push) Has been cancelled
CI / Frontend CI (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / CI Summary (push) Has been cancelled
[SIMCO-V38] feat: Actualizar a SIMCO v3.8.0
- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8
- Actualizaciones de configuracion

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

489 lines
16 KiB
Markdown

---
id: "VIS-004"
title: "Arquitectura Multi-Tenant"
type: "Architecture"
status: "Published"
priority: "P0"
version: "1.0.0"
created_date: "2026-01-07"
updated_date: "2026-01-10"
---
# ARQUITECTURA MULTI-TENANT
**Proyecto:** template-saas
**Version:** 1.0.0
**Tipo:** Especificacion Tecnica
---
## Modelo de Multi-Tenancy
Este template implementa **multi-tenancy a nivel de base de datos** usando PostgreSQL Row-Level Security (RLS).
### Estrategia: Shared Database, Shared Schema
```
┌─────────────────────────────────────────────────────────────┐
│ PostgreSQL Instance │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Database: saas_db │ │
│ │ ┌──────────┬──────────┬──────────┬──────────────────┐ │ │
│ │ │ Tenant A │ Tenant B │ Tenant C │ ... Tenant N │ │ │
│ │ │ (RLS) │ (RLS) │ (RLS) │ (RLS) │ │ │
│ │ └──────────┴──────────┴──────────┴──────────────────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### Ventajas
- **Simplicidad operacional** - Una sola base de datos
- **Eficiencia de recursos** - Conexiones compartidas
- **Mantenimiento facil** - Migraciones unicas
- **Escalabilidad** - Cientos de tenants sin overhead
### Consideraciones
- RLS DEBE estar habilitado en TODAS las tablas con datos de tenant
- Cada query DEBE incluir el `tenant_id` en el contexto
- Backups incluyen todos los tenants (encriptacion requerida)
---
## Implementacion RLS
### Estructura Base de Tablas
```sql
-- Todas las tablas de tenant heredan esta estructura
CREATE TABLE tenants.base_entity (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
created_by UUID,
updated_by UUID
);
-- Indice obligatorio en tenant_id
CREATE INDEX idx_base_entity_tenant ON tenants.base_entity(tenant_id);
```
### Politicas RLS Estandar
```sql
-- Habilitar RLS
ALTER TABLE {schema}.{table} ENABLE ROW LEVEL SECURITY;
-- Politica de lectura
CREATE POLICY "{table}_tenant_isolation_select" ON {schema}.{table}
FOR SELECT
USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
-- Politica de insercion
CREATE POLICY "{table}_tenant_isolation_insert" ON {schema}.{table}
FOR INSERT
WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::UUID);
-- Politica de actualizacion
CREATE POLICY "{table}_tenant_isolation_update" ON {schema}.{table}
FOR UPDATE
USING (tenant_id = current_setting('app.current_tenant_id')::UUID)
WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::UUID);
-- Politica de eliminacion
CREATE POLICY "{table}_tenant_isolation_delete" ON {schema}.{table}
FOR DELETE
USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
```
### Configuracion de Contexto
```sql
-- Funcion para establecer tenant actual
CREATE OR REPLACE FUNCTION auth.set_current_tenant(p_tenant_id UUID)
RETURNS VOID AS $$
BEGIN
PERFORM set_config('app.current_tenant_id', p_tenant_id::TEXT, FALSE);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Funcion para obtener tenant actual
CREATE OR REPLACE FUNCTION auth.get_current_tenant()
RETURNS UUID AS $$
BEGIN
RETURN current_setting('app.current_tenant_id', TRUE)::UUID;
END;
$$ LANGUAGE plpgsql STABLE;
```
---
## Arquitectura Backend
### Middleware de Tenant
```typescript
// src/shared/middleware/tenant.middleware.ts
@Injectable()
export class TenantMiddleware implements NestMiddleware {
constructor(
private readonly tenantService: TenantService,
private readonly dataSource: DataSource,
) {}
async use(req: Request, res: Response, next: NextFunction) {
const tenantId = this.extractTenantId(req);
if (!tenantId) {
throw new UnauthorizedException('Tenant not identified');
}
// Validar que el tenant existe y esta activo
const tenant = await this.tenantService.findById(tenantId);
if (!tenant || tenant.status !== 'active') {
throw new ForbiddenException('Tenant inactive or not found');
}
// Establecer en request para uso posterior
req['tenant'] = tenant;
// Establecer en PostgreSQL para RLS
await this.dataSource.query(
`SELECT auth.set_current_tenant($1)`,
[tenantId]
);
next();
}
private extractTenantId(req: Request): string | null {
// 1. Desde JWT token
if (req.user?.tenantId) {
return req.user.tenantId;
}
// 2. Desde header
const headerTenant = req.headers['x-tenant-id'];
if (headerTenant) {
return headerTenant as string;
}
// 3. Desde subdominio
const host = req.headers.host;
if (host) {
const subdomain = host.split('.')[0];
// Lookup tenant by subdomain
}
return null;
}
}
```
### Guard de Tenant
```typescript
// src/shared/guards/tenant.guard.ts
@Injectable()
export class TenantGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
if (!request.tenant) {
throw new ForbiddenException('Tenant context required');
}
return true;
}
}
```
### Decorador de Tenant
```typescript
// src/shared/decorators/tenant.decorator.ts
export const CurrentTenant = createParamDecorator(
(data: unknown, ctx: ExecutionContext): Tenant => {
const request = ctx.switchToHttp().getRequest();
return request.tenant;
},
);
// Uso en controlador
@Get('products')
@UseGuards(TenantGuard)
async getProducts(@CurrentTenant() tenant: Tenant) {
return this.productService.findAll(tenant.id);
}
```
---
## Arquitectura de Schemas
```
┌─────────────────────────────────────────────────────────────┐
│ DATABASE SCHEMAS │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ auth │ │ tenants │ │ users │ │
│ │ │ │ │ │ │ │
│ │ - sessions │ │ - tenants │ │ - users │ │
│ │ - tokens │ │ - settings │ │ - roles │ │
│ │ - oauth │ │ - domains │ │ - permissions │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ billing │ │ plans │ │ audit │ │
│ │ │ │ │ │ │ │
│ │ - invoices │ │ - plans │ │ - audit_logs │ │
│ │ - payments │ │ - features │ │ - activity_logs │ │
│ │ - subscript │ │ - limits │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │notifications│ │ storage │ │ webhooks │ │
│ │ │ │ │ │ │ │
│ │ - templates │ │ - files │ │ - endpoints │ │
│ │ - queue │ │ - metadata │ │ - deliveries │ │
│ │ - logs │ │ │ │ - logs │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## Flujo de Autenticacion
```
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Client │────▶│ Login │────▶│ Verify │────▶│ Issue │
│ │ │ Request │ │ Creds │ │ JWT │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Response │◀────│ Set │◀────│ Create │◀────│ Token │
│ + JWT │ │ Tenant │ │ Session │ │ contains │
│ │ │ Context │ │ │ │ tenantId │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
```
### JWT Payload
```typescript
interface JwtPayload {
sub: string; // user_id
tenantId: string; // tenant_id
email: string;
roles: string[];
permissions: string[];
iat: number;
exp: number;
}
```
---
## Modelo de Datos Principal
### Tenant
```sql
CREATE TABLE tenants.tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
domain VARCHAR(255) UNIQUE,
status VARCHAR(50) DEFAULT 'active',
plan_id UUID REFERENCES plans.plans(id),
settings JSONB DEFAULT '{}',
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT valid_status CHECK (status IN ('active', 'suspended', 'cancelled'))
);
```
### User (pertenece a Tenant)
```sql
CREATE TABLE users.users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id),
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255),
first_name VARCHAR(100),
last_name VARCHAR(100),
status VARCHAR(50) DEFAULT 'active',
email_verified BOOLEAN DEFAULT FALSE,
last_login TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT unique_email_per_tenant UNIQUE (tenant_id, email)
);
-- RLS
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);
```
### Plan
```sql
CREATE TABLE plans.plans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
slug VARCHAR(50) UNIQUE NOT NULL,
description TEXT,
price_monthly DECIMAL(10,2),
price_yearly DECIMAL(10,2),
stripe_price_id_monthly VARCHAR(255),
stripe_price_id_yearly VARCHAR(255),
features JSONB DEFAULT '[]',
limits JSONB DEFAULT '{}',
is_active BOOLEAN DEFAULT TRUE,
sort_order INT DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Ejemplo de limits JSONB
-- {
-- "max_users": 5,
-- "max_storage_gb": 10,
-- "max_api_calls_month": 10000,
-- "max_products": 100
-- }
```
---
## Integracion Stripe
### Flujo de Suscripcion
```
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Tenant │────▶│ Select │────▶│ Stripe │────▶│ Webhook │
│ Signup │ │ Plan │ │ Checkout │ │ Received │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Tenant │◀────│ Update │◀────│ Create │◀────│ Process │
│ Active │ │ Status │ │ Invoice │ │ Event │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
```
### Webhooks de Stripe
| Evento | Accion |
|--------|--------|
| `checkout.session.completed` | Activar suscripcion |
| `customer.subscription.updated` | Actualizar plan |
| `customer.subscription.deleted` | Cancelar suscripcion |
| `invoice.paid` | Registrar pago |
| `invoice.payment_failed` | Notificar y suspender |
---
## Seguridad
### Capas de Seguridad
1. **Network** - HTTPS obligatorio, rate limiting
2. **Authentication** - JWT, OAuth, MFA
3. **Authorization** - RBAC, tenant isolation
4. **Database** - RLS, conexiones encriptadas
5. **Application** - Input validation, OWASP
### Headers de Seguridad
```typescript
// Helmet configuration
app.use(helmet({
contentSecurityPolicy: true,
crossOriginEmbedderPolicy: true,
crossOriginOpenerPolicy: true,
crossOriginResourcePolicy: true,
dnsPrefetchControl: true,
frameguard: true,
hidePoweredBy: true,
hsts: true,
ieNoOpen: true,
noSniff: true,
originAgentCluster: true,
permittedCrossDomainPolicies: true,
referrerPolicy: true,
xssFilter: true,
}));
```
---
## Escalabilidad
### Horizontal
- Backend stateless (multiples instancias)
- Redis para sesiones y cache
- PostgreSQL read replicas
- CDN para assets estaticos
### Vertical
- Connection pooling (PgBouncer)
- Query optimization
- Indices estrategicos
- Particionamiento de tablas grandes
### Por Tenant
- Rate limiting por tenant
- Cuotas de almacenamiento
- Limites de API calls
- Fair usage policies
---
## Monitoreo
### Metricas Clave
- Requests por tenant
- Latencia de API
- Errores por tenant
- Uso de recursos
- Conexiones de DB
### Alertas
- Tenant excede cuota
- Error rate alto
- Latencia degradada
- DB connections altas
---
## Referencias
- Vision: `VISION-TEMPLATE-SAAS.md`
- Inventarios: `../../orchestration/inventarios/`
- Database DDL: `../../apps/database/ddl/`
---
**Creado:** 2026-01-07
**Actualizado:** 2026-01-07