- Configure workspace Git repository with comprehensive .gitignore - Add Odoo as submodule for ERP reference code - Include documentation: SETUP.md, GIT-STRUCTURE.md - Add gitignore templates for projects (backend, frontend, database) - Structure supports independent repos per project/subproject level Workspace includes: - core/ - Reusable patterns, modules, orchestration system - projects/ - Active projects (erp-suite, gamilit, trading-platform, etc.) - knowledge-base/ - Reference code and patterns (includes Odoo submodule) - devtools/ - Development tools and templates - customers/ - Client implementations template 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| IMPLEMENTATION.md | ||
| README.md | ||
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
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)
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)
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
// 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
// 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
// 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
@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
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
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:
-- 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
# 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
- Método de detección: Header, subdomain, o query param
- Roles: Ajustar según necesidades (owner, admin, etc.)
- Suscripciones: Definir tiers y límites
- Settings: Estructura de configuración por tenant
- RLS: Implementar si se requiere seguridad extra
Referencias
Mantenido por: Sistema NEXUS Proyecto origen: Gamilit Platform