# Soporte Multi-Tenant **Versión:** 1.0.0 **Origen:** projects/gamilit **Estado:** Producción **Última actualización:** 2025-12-08 --- ## Descripción Sistema de multi-tenancy para aplicaciones SaaS: - Aislamiento de datos por organización - Usuarios pueden pertenecer a múltiples tenants - Roles específicos por tenant (owner, admin, member) - Configuración y personalización por tenant - Límites de recursos (usuarios, storage) - Niveles de suscripción --- ## Características | Característica | Descripción | |----------------|-------------| | Aislamiento | Datos separados por tenant | | Multi-membresía | Usuario en múltiples tenants | | Roles por tenant | owner, admin, member, viewer | | Suscripciones | free, basic, pro, enterprise | | Personalización | Theme, logo, dominio custom | | Límites | Máx usuarios, storage | | RLS (opcional) | Row Level Security en PostgreSQL | --- ## Stack Tecnológico ```yaml backend: framework: NestJS orm: TypeORM database: PostgreSQL patterns: - Tenant discriminator (tenant_id en tablas) - Middleware de tenant context - Optional: RLS para seguridad extra ``` --- ## Tablas Requeridas | Tabla | Propósito | |-------|-----------| | auth_management.tenants | Organizaciones/empresas | | auth_management.memberships | Relación usuario-tenant | | auth_management.profiles | Perfil extendido con tenant_id | --- ## Estructura del Módulo ``` multi-tenancy/ ├── entities/ │ ├── tenant.entity.ts │ └── membership.entity.ts ├── services/ │ ├── tenant.service.ts │ └── membership.service.ts ├── middleware/ │ └── tenant-context.middleware.ts ├── guards/ │ └── tenant-member.guard.ts ├── decorators/ │ └── current-tenant.decorator.ts └── dto/ ├── create-tenant.dto.ts └── membership.dto.ts ``` --- ## Modelos de Datos ### Tenant (Organización) ```typescript interface Tenant { id: string; // UUID name: string; // "Empresa XYZ" slug: string; // "empresa-xyz" (único) domain?: string; // "xyz.app.com" logo_url?: string; subscription_tier: 'free' | 'basic' | 'pro' | 'enterprise'; max_users: number; // Límite de usuarios max_storage_gb: number; // Límite de storage is_active: boolean; trial_ends_at?: Date; settings: { // Configuración JSONB theme: string; features: Record; language: string; timezone: string; }; metadata: Record; } ``` ### Membership (Usuario-Tenant) ```typescript interface Membership { id: string; user_id: string; tenant_id: string; role: 'owner' | 'admin' | 'member' | 'viewer'; status: 'pending' | 'active' | 'suspended'; invited_by?: string; joined_at: Date; } ``` --- ## Flujo de Multi-Tenancy ``` 1. Usuario autenticado │ ▼ 2. Middleware extrae tenant de: - Header: X-Tenant-ID - Subdomain: xyz.app.com → "xyz" - Query param: ?tenant=xyz │ ▼ 3. Verificar membresía activa │ ▼ 4. Inyectar tenant_id en contexto │ ▼ 5. Queries filtran por tenant_id ``` --- ## Uso Rápido ### 1. Middleware de Tenant ```typescript // src/common/middleware/tenant-context.middleware.ts @Injectable() export class TenantContextMiddleware implements NestMiddleware { constructor( private readonly membershipService: MembershipService, ) {} async use(req: Request, res: Response, next: NextFunction) { const tenantId = this.extractTenantId(req); if (!tenantId) { return next(); // Rutas sin tenant } // Verificar membresía si hay usuario if (req.user?.id) { const membership = await this.membershipService.findByUserAndTenant( req.user.id, tenantId, ); if (!membership || membership.status !== 'active') { throw new ForbiddenException('No tienes acceso a este tenant'); } req.tenantContext = { tenantId, role: membership.role, }; } next(); } private extractTenantId(req: Request): string | null { // Opción 1: Header const headerTenant = req.headers['x-tenant-id'] as string; if (headerTenant) return headerTenant; // Opción 2: Subdomain const host = req.hostname; const subdomain = host.split('.')[0]; if (subdomain && subdomain !== 'www' && subdomain !== 'app') { return subdomain; } // Opción 3: Query param return req.query.tenant as string; } } ``` ### 2. Guard de Membresía ```typescript // src/common/guards/tenant-member.guard.ts @Injectable() export class TenantMemberGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const req = context.switchToHttp().getRequest(); if (!req.tenantContext) { throw new ForbiddenException('Tenant context required'); } return true; } } // Guard con rol específico @Injectable() export class TenantAdminGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const req = context.switchToHttp().getRequest(); if (!req.tenantContext) { throw new ForbiddenException('Tenant context required'); } const allowedRoles = ['owner', 'admin']; if (!allowedRoles.includes(req.tenantContext.role)) { throw new ForbiddenException('Admin role required'); } return true; } } ``` ### 3. Decorador para obtener tenant ```typescript // src/common/decorators/current-tenant.decorator.ts export const CurrentTenant = createParamDecorator( (data: unknown, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); return request.tenantContext; }, ); // Uso en controller @Get('data') @UseGuards(JwtAuthGuard, TenantMemberGuard) getData(@CurrentTenant() tenant: TenantContext) { return this.service.findByTenant(tenant.tenantId); } ``` ### 4. Service con filtro de tenant ```typescript @Injectable() export class ProjectService { async findAll(tenantId: string): Promise { return this.projectRepository.find({ where: { tenant_id: tenantId }, }); } async create(tenantId: string, dto: CreateProjectDto): Promise { const project = this.projectRepository.create({ ...dto, tenant_id: tenantId, // Siempre asignar tenant }); return this.projectRepository.save(project); } } ``` --- ## Crear Nuevo Tenant ```typescript async createTenant(dto: CreateTenantDto, ownerId: string): Promise { // 1. Crear tenant const tenant = this.tenantRepository.create({ name: dto.name, slug: this.generateSlug(dto.name), subscription_tier: 'free', max_users: 10, max_storage_gb: 1, settings: { theme: 'default', features: { analytics: true }, language: 'es', }, }); await this.tenantRepository.save(tenant); // 2. Crear membresía de owner const membership = this.membershipRepository.create({ user_id: ownerId, tenant_id: tenant.id, role: 'owner', status: 'active', joined_at: new Date(), }); await this.membershipRepository.save(membership); return tenant; } ``` --- ## Invitar Usuario a Tenant ```typescript async inviteUser( tenantId: string, inviterId: string, email: string, role: string, ): Promise { // 1. Buscar usuario por email const user = await this.userRepository.findOne({ where: { email } }); if (!user) { // Enviar invitación por email para registro await this.mailService.sendInvitation(email, tenantId, role); return; } // 2. Verificar si ya es miembro const existing = await this.membershipRepository.findOne({ where: { user_id: user.id, tenant_id: tenantId }, }); if (existing) { throw new ConflictException('Usuario ya es miembro'); } // 3. Crear membresía const membership = this.membershipRepository.create({ user_id: user.id, tenant_id: tenantId, role, status: 'active', invited_by: inviterId, joined_at: new Date(), }); await this.membershipRepository.save(membership); } ``` --- ## Row Level Security (Opcional) Para seguridad adicional a nivel de base de datos: ```sql -- Habilitar RLS en tabla ALTER TABLE projects ENABLE ROW LEVEL SECURITY; -- Policy: usuarios solo ven proyectos de sus tenants CREATE POLICY tenant_isolation ON projects USING ( tenant_id IN ( SELECT tenant_id FROM memberships WHERE user_id = current_setting('app.current_user_id')::uuid AND status = 'active' ) ); -- Antes de cada query, setear el user_id SET app.current_user_id = 'user-uuid'; ``` --- ## Variables de Entorno ```env # Multi-tenancy ENABLE_MULTITENANCY=true DEFAULT_TENANT_SLUG=main # Límites por defecto DEFAULT_MAX_USERS=100 DEFAULT_MAX_STORAGE_GB=5 ``` --- ## Endpoints Principales | Método | Ruta | Descripción | |--------|------|-------------| | GET | /tenants | Listar tenants del usuario | | POST | /tenants | Crear nuevo tenant | | GET | /tenants/:id | Obtener detalle de tenant | | PUT | /tenants/:id | Actualizar tenant (admin) | | POST | /tenants/:id/invite | Invitar usuario | | GET | /tenants/:id/members | Listar miembros | | PUT | /tenants/:id/members/:userId | Cambiar rol | | DELETE | /tenants/:id/members/:userId | Remover miembro | --- ## Adaptaciones Necesarias 1. **Método de detección**: Header, subdomain, o query param 2. **Roles**: Ajustar según necesidades (owner, admin, etc.) 3. **Suscripciones**: Definir tiers y límites 4. **Settings**: Estructura de configuración por tenant 5. **RLS**: Implementar si se requiere seguridad extra --- ## Referencias - [NestJS Multi-Tenancy Patterns](https://docs.nestjs.com/) - [PostgreSQL Row Level Security](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) --- **Mantenido por:** Sistema NEXUS **Proyecto origen:** Gamilit Platform