- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8 - Actualizaciones de configuracion Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
489 lines
16 KiB
Markdown
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
|