# Directiva: Multi-Tenant ## Propósito Define las reglas obligatorias para implementar multi-tenancy en ERP Core y todas las verticales que lo extienden. ## Alcance - Backend: Todas las consultas y operaciones - Database: RLS Policies, schemas, índices - Frontend: Contexto de tenant - APIs: Headers y validación --- ## Principios ### 1. Aislamiento Total ``` OBLIGATORIO: Ningún tenant puede ver o modificar datos de otro tenant. ``` ### 2. Tenant ID Obligatorio ``` OBLIGATORIO: Toda tabla debe tener columna `tenant_id UUID NOT NULL`. ``` ### 3. Filtrado Automático ``` OBLIGATORIO: Toda consulta debe filtrar por tenant_id a nivel de aplicación Y base de datos (RLS). ``` --- ## Implementación en Base de Datos ### Columna tenant_id ```sql -- OBLIGATORIO en TODA tabla CREATE TABLE {schema}.{tabla} ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id), -- ... otras columnas ... created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- Índice obligatorio CREATE INDEX idx_{tabla}_tenant_id ON {schema}.{tabla}(tenant_id); ``` ### RLS Policy Estándar ```sql -- Habilitar RLS ALTER TABLE {schema}.{tabla} ENABLE ROW LEVEL SECURITY; -- Policy de lectura CREATE POLICY "tenant_isolation_select" ON {schema}.{tabla} FOR SELECT USING (tenant_id = current_setting('app.current_tenant_id')::uuid); -- Policy de inserción CREATE POLICY "tenant_isolation_insert" ON {schema}.{tabla} FOR INSERT WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::uuid); -- Policy de actualización CREATE POLICY "tenant_isolation_update" ON {schema}.{tabla} FOR UPDATE USING (tenant_id = current_setting('app.current_tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::uuid); -- Policy de eliminación CREATE POLICY "tenant_isolation_delete" ON {schema}.{tabla} FOR DELETE USING (tenant_id = current_setting('app.current_tenant_id')::uuid); ``` ### Contexto de Sesión ```sql -- Establecer contexto al inicio de cada request SET app.current_tenant_id = '{tenant_uuid}'; -- En transacciones BEGIN; SET LOCAL app.current_tenant_id = '{tenant_uuid}'; -- ... operaciones ... COMMIT; ``` --- ## Implementación en Backend ### Middleware de Tenant ```typescript // middleware/tenant.middleware.ts export const tenantMiddleware = async (req, res, next) => { const tenantId = req.headers['x-tenant-id'] || req.user?.tenantId; if (!tenantId) { return res.status(400).json({ error: 'Tenant ID required' }); } // Validar que tenant existe y está activo const tenant = await tenantService.findById(tenantId); if (!tenant || !tenant.isActive) { return res.status(403).json({ error: 'Invalid or inactive tenant' }); } req.tenantId = tenantId; // Establecer contexto en BD await dbConnection.query(`SET app.current_tenant_id = '${tenantId}'`); next(); }; ``` ### En Servicios ```typescript // OBLIGATORIO: Siempre filtrar por tenantId async findAll(tenantId: string): Promise { return this.repository.find({ where: { tenantId } }); } // OBLIGATORIO: Validar tenantId en creación async create(dto: CreateDto, tenantId: string): Promise { const entity = this.repository.create({ ...dto, tenantId }); return this.repository.save(entity); } ``` ### En Controllers ```typescript @Get() async findAll(@Req() req: Request) { // tenantId viene del middleware return this.service.findAll(req.tenantId); } ``` --- ## Implementación en Frontend ### Contexto de Tenant ```typescript // stores/tenant.store.ts interface TenantState { currentTenantId: string | null; tenants: Tenant[]; } export const useTenantStore = create((set) => ({ currentTenantId: null, tenants: [], setCurrentTenant: (id) => set({ currentTenantId: id }), })); ``` ### En API Calls ```typescript // services/api.service.ts const api = axios.create({ baseURL: API_URL, }); api.interceptors.request.use((config) => { const tenantId = useTenantStore.getState().currentTenantId; if (tenantId) { config.headers['X-Tenant-Id'] = tenantId; } return config; }); ``` --- ## Validaciones Obligatorias ### Checklist Pre-Deploy - [ ] Toda tabla tiene columna `tenant_id` - [ ] Toda tabla tiene RLS habilitado - [ ] Toda tabla tiene policies de tenant - [ ] Todo endpoint usa middleware de tenant - [ ] Todo servicio filtra por tenant - [ ] Frontend envía header X-Tenant-Id ### Tests Obligatorios ```typescript describe('Tenant Isolation', () => { it('should not return data from other tenants', async () => { // Crear datos en tenant A await service.create(data, tenantA); // Consultar desde tenant B const results = await service.findAll(tenantB); // No debe encontrar datos de tenant A expect(results).toHaveLength(0); }); }); ``` --- ## Excepciones Las siguientes tablas NO requieren tenant_id: - `core_tenants.tenants` (la tabla maestra de tenants) - `core_auth.system_settings` (configuración global del sistema) - `core_catalogs.countries`, `core_catalogs.currencies` (catálogos globales) --- ## Referencias - RLS PostgreSQL: https://www.postgresql.org/docs/current/ddl-rowsecurity.html - Directiva Diseño BD: `DIRECTIVA-DISENO-BASE-DATOS.md` --- *Directiva específica de ERP-Core*