# 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<string, boolean>;
    language: string;
    timezone: string;
  };
  metadata: Record<string, any>;
}
```

### 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<Project[]> {
    return this.projectRepository.find({
      where: { tenant_id: tenantId },
    });
  }

  async create(tenantId: string, dto: CreateProjectDto): Promise<Project> {
    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<Tenant> {
  // 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<void> {
  // 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
