# Patron Multi-Tenant con RLS **Categoria:** Architecture **Version:** 1.0.0 **Origen:** projects/gamilit, projects/erp-core **Fecha:** 2025-12-27 --- ## Descripcion Este documento describe el patron de arquitectura multi-tenant utilizado en todos los proyectos del workspace, basado en Row-Level Security (RLS) de PostgreSQL. ## Principios 1. **Aislamiento completo**: Cada tenant solo ve sus propios datos 2. **Transparencia**: El codigo de aplicacion no necesita filtrar por tenant 3. **Performance**: RLS optimizado con indices apropiados 4. **Seguridad**: Imposible acceder a datos de otros tenants ## Arquitectura ``` ┌─────────────────────────────────┐ │ Application │ │ (NestJS / Express) │ └───────────────┬─────────────────┘ │ ┌───────────────▼─────────────────┐ │ Tenant Middleware │ │ SET app.current_tenant_id │ └───────────────┬─────────────────┘ │ ┌───────────────▼─────────────────┐ │ PostgreSQL │ │ Row-Level Security │ │ (USING tenant_id = ...) │ └─────────────────────────────────┘ ``` ## Implementacion ### 1. Variable de Sesion ```sql -- Configurar en cada request SET app.current_tenant_id = 'uuid-del-tenant'; -- Obtener en queries current_setting('app.current_tenant_id', true)::UUID ``` ### 2. Middleware NestJS ```typescript // src/middleware/tenant.middleware.ts @Injectable() export class TenantMiddleware implements NestMiddleware { constructor(private dataSource: DataSource) {} async use(req: Request, res: Response, next: NextFunction) { const tenantId = this.extractTenantId(req); if (tenantId) { await this.dataSource.query( `SET app.current_tenant_id = $1`, [tenantId] ); } next(); } private extractTenantId(req: Request): string | null { // 1. Del JWT token if (req.user?.tenantId) return req.user.tenantId; // 2. Del header if (req.headers['x-tenant-id']) return req.headers['x-tenant-id']; // 3. Del subdominio const host = req.headers.host; // tenant.example.com -> tenant return null; } } ``` ### 3. RLS Policies ```sql -- Habilitar RLS en tabla ALTER TABLE my_table ENABLE ROW LEVEL SECURITY; -- Politica de aislamiento CREATE POLICY tenant_isolation ON my_table FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -- Para INSERT, asegurar que se use el tenant correcto CREATE POLICY tenant_insert ON my_table FOR INSERT WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::UUID); ``` ### 4. Estructura de Tabla ```sql CREATE TABLE my_table ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id), -- ... otros campos ... created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- Indice para performance CREATE INDEX idx_my_table_tenant ON my_table(tenant_id); ``` ## Best Practices ### DO (Hacer) - Siempre incluir `tenant_id` en tablas de datos - Crear indice en `tenant_id` - Habilitar RLS en TODAS las tablas con datos de tenant - Usar el middleware en TODAS las rutas protegidas - Testear aislamiento en cada nueva tabla ### DON'T (No hacer) - No filtrar manualmente por tenant_id (dejar a RLS) - No desactivar RLS para "queries rapidas" - No hardcodear tenant_id en codigo - No compartir conexiones entre requests sin reset ## Tablas Exentas de RLS Algunas tablas son globales y no tienen tenant_id: - `auth.tenants` - Lista de todos los tenants - `config.feature_flags` - Flags globales - `billing.plans` - Planes disponibles ## Testing ```typescript describe('Tenant Isolation', () => { it('should not see data from other tenant', async () => { // Crear datos en tenant A await setTenant(tenantA); await createRecord({ name: 'Record A' }); // Cambiar a tenant B await setTenant(tenantB); const records = await getRecords(); // No debe ver record de tenant A expect(records).not.toContainEqual( expect.objectContaining({ name: 'Record A' }) ); }); }); ``` ## Troubleshooting ### Error: "Permission denied for table" - Verificar que RLS esta habilitado - Verificar que la politica existe - Verificar que `app.current_tenant_id` esta configurado ### Query retorna todos los registros - Verificar que el middleware esta activo - Verificar que `current_setting` retorna el UUID correcto - Verificar que la politica no tiene errores ### Performance lenta - Crear indice en `tenant_id` - Verificar que el indice se usa (EXPLAIN ANALYZE) - Considerar particionamiento por tenant_id ## Referencias - `shared/catalog/multi-tenancy/` - Modulo del catalogo - `projects/erp-core/database/ddl/` - Ejemplos de DDL - PostgreSQL RLS Documentation --- *Knowledge Base - Workspace v1*