# Directiva: Arquitectura Multi-Tenant PMC **Version:** 1.0.0 **Fecha:** 2025-12-08 **Estado:** Activa **Referencia Catalogo:** @CATALOG_TENANT --- ## Proposito Define la estrategia de multi-tenancy para PMC, garantizando aislamiento de datos entre organizaciones (agencias) mientras se mantiene eficiencia operativa. --- ## Estrategia Seleccionada **Tipo:** Single Database, Shared Schema con Row-Level Security (RLS) **Justificacion:** Ver ADR-002-multi-tenancy.md --- ## Principios ### P1: Aislamiento Total de Datos Todo dato de negocio debe estar asociado a un tenant y ser invisible para otros tenants. ```sql -- OBLIGATORIO en todas las tablas de negocio tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ``` ### P2: Filtrado Automatico El filtrado por tenant debe ser automatico, no depender del desarrollador. ```sql -- RLS policy en cada tabla CREATE POLICY tenant_isolation ON {tabla} USING (tenant_id = current_setting('app.current_tenant')::uuid); ``` ### P3: Context Propagation El tenant_id debe propagarse automaticamente en cada request. ```typescript // Middleware extrae tenant del JWT const tenantId = req.user.tenantId; await dataSource.query(`SET app.current_tenant = '${tenantId}'`); ``` ### P4: Storage Isolation Archivos deben estar segregados por tenant en storage. ``` bucket/{tenant_slug}/assets/ bucket/{tenant_slug}/generated/ bucket/{tenant_slug}/models/ ``` --- ## Implementacion ### 1. Middleware de Tenant ```typescript @Injectable() export class TenantMiddleware implements NestMiddleware { constructor(private dataSource: DataSource) {} async use(req: Request, res: Response, next: NextFunction) { const tenantId = req.user?.tenantId; if (tenantId) { // Set para RLS await this.dataSource.query( `SET app.current_tenant = '${tenantId}'` ); // Agregar al request context req.tenantContext = { tenantId }; } next(); } } ``` ### 2. Decorador de Tenant ```typescript export const CurrentTenant = createParamDecorator( (data: unknown, ctx: ExecutionContext): string => { const request = ctx.switchToHttp().getRequest(); return request.tenantContext?.tenantId; }, ); // Uso @Get() async findAll(@CurrentTenant() tenantId: string) { // tenantId ya disponible } ``` ### 3. BaseEntity con tenant_id ```typescript export abstract class TenantAwareEntity { @Column('uuid') tenant_id: string; @CreateDateColumn() created_at: Date; @UpdateDateColumn() updated_at: Date; @DeleteDateColumn() deleted_at: Date; } // Uso @Entity('clients', { schema: 'crm' }) export class Client extends TenantAwareEntity { @PrimaryGeneratedColumn('uuid') id: string; @Column() name: string; } ``` ### 4. Service Base con Tenant ```typescript export abstract class TenantAwareService { constructor( protected readonly repository: Repository, ) {} async findAllByTenant(tenantId: string): Promise { return this.repository.find({ where: { tenant_id: tenantId } as any, }); } async createForTenant(tenantId: string, dto: any): Promise { const entity = this.repository.create({ ...dto, tenant_id: tenantId, }); return this.repository.save(entity); } } ``` --- ## Tablas sin tenant_id Las siguientes tablas son globales (no tienen tenant_id): | Tabla | Razon | |-------|-------| | auth.tenants | Es la tabla de tenants | | auth.plans | Planes son globales | | generation.workflow_templates (is_system=true) | Templates del sistema | | config.feature_flags (tenant_id IS NULL) | Flags globales | --- ## Validaciones Obligatorias ### En Creacion de Entidad ```typescript // SIEMPRE asignar tenant_id en creacion const client = this.clientRepository.create({ ...createClientDto, tenant_id: tenantId, // OBLIGATORIO }); ``` ### En Queries ```typescript // SIEMPRE filtrar por tenant (aunque RLS lo haga) const clients = await this.clientRepository.find({ where: { tenant_id: tenantId }, }); // NUNCA queries sin filtro de tenant en tablas tenant-aware // INCORRECTO: const allClients = await this.clientRepository.find(); // PROHIBIDO ``` ### En Updates/Deletes ```typescript // SIEMPRE verificar propiedad antes de modificar const client = await this.clientRepository.findOne({ where: { id: clientId, tenant_id: tenantId }, }); if (!client) { throw new NotFoundException('Client not found'); } ``` --- ## Limites y Quotas por Tenant ```typescript interface TenantLimits { max_users: number; max_brands: number; max_products: number; max_storage_gb: number; generations_per_month: number; trainings_per_month: number; } // Verificar antes de operaciones async checkQuota(tenantId: string, resource: string): Promise { const tenant = await this.tenantService.findById(tenantId); const usage = await this.usageService.getCurrentUsage(tenantId, resource); return usage < tenant.limits[resource]; } ``` --- ## Testing Multi-Tenant ```typescript describe('ClientService (multi-tenant)', () => { it('should not return clients from other tenants', async () => { // Crear clientes en tenant A y B await service.create(tenantA, { name: 'Client A' }); await service.create(tenantB, { name: 'Client B' }); // Query desde tenant A const clients = await service.findAll(tenantA); // Solo debe ver sus clientes expect(clients).toHaveLength(1); expect(clients[0].name).toBe('Client A'); }); }); ``` --- ## Errores Comunes ### ERROR 1: Olvidar tenant_id en INSERT ```typescript // INCORRECTO const brand = this.brandRepository.create({ name: dto.name, // Falta tenant_id! }); // CORRECTO const brand = this.brandRepository.create({ name: dto.name, tenant_id: tenantId, }); ``` ### ERROR 2: Query sin filtro de tenant ```typescript // INCORRECTO - Expone datos de otros tenants si RLS falla const brand = await this.brandRepository.findOne({ where: { id: brandId }, }); // CORRECTO const brand = await this.brandRepository.findOne({ where: { id: brandId, tenant_id: tenantId }, }); ``` ### ERROR 3: No validar propiedad en acciones ```typescript // INCORRECTO - Puede modificar recursos de otro tenant async delete(brandId: string) { await this.brandRepository.delete(brandId); } // CORRECTO async delete(tenantId: string, brandId: string) { const result = await this.brandRepository.delete({ id: brandId, tenant_id: tenantId, }); if (result.affected === 0) { throw new NotFoundException(); } } ``` --- ## Checklist de Implementacion - [ ] Tabla tiene columna tenant_id - [ ] Foreign key a auth.tenants - [ ] Index en tenant_id - [ ] RLS policy creada - [ ] Entity extiende TenantAwareEntity - [ ] Service usa TenantAwareService base - [ ] Controller usa @CurrentTenant() - [ ] Tests verifican aislamiento - [ ] Storage usa prefijo de tenant --- ## Referencias - Catalogo: `shared/catalog/multi-tenancy/README.md` - ADR: `docs/97-adr/ADR-002-multi-tenancy.md` - Patrones RLS: `shared/catalog/multi-tenancy/patterns/rls-policies.md` --- **Generado por:** Requirements-Analyst **Fecha:** 2025-12-08