platform-marketing-content/orchestration/directivas/DIRECTIVA-ARQUITECTURA-MULTI-TENANT.md
rckrdmrd 74b5ed7f38 feat: Complete documentation update and orchestration configuration
- Update vision, architecture and technical documentation
- Update module definitions (PMC-001 to PMC-008)
- Update requirements documentation
- Add CONTEXT-MAP.yml and ENVIRONMENT-INVENTORY.yml
- Add orchestration guidelines and references

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 05:38:31 -06:00

341 lines
7.0 KiB
Markdown

# 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<T> {
constructor(
protected readonly repository: Repository<T>,
) {}
async findAllByTenant(tenantId: string): Promise<T[]> {
return this.repository.find({
where: { tenant_id: tenantId } as any,
});
}
async createForTenant(tenantId: string, dto: any): Promise<T> {
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<boolean> {
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