- 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>
7.0 KiB
7.0 KiB
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.
-- 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.
-- 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.
// 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
@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
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
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
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
// SIEMPRE asignar tenant_id en creacion
const client = this.clientRepository.create({
...createClientDto,
tenant_id: tenantId, // OBLIGATORIO
});
En Queries
// 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
// 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
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
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
// 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
// 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
// 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