# 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` - Database: `../../database/README.md` - Inventarios: `../../orchestration/inventarios/` --- **Creado:** 2026-01-07 **Actualizado:** 2026-01-07