JSPM

nestjs-multitenant

2.1.1
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 15
  • Score
    100M100P100Q59324F
  • License MIT

A comprehensive multi-tenant solution for NestJS applications with PostgreSQL schema-per-tenant architecture

Package Exports

  • nestjs-multitenant
  • nestjs-multitenant/package.json

Readme

nestjs-multitenant

Una solución completa de multi-tenancy para aplicaciones NestJS con arquitectura de esquema por tenant (PostgreSQL) y utilidades para inyección de repositorios, middlewares de resolución de tenant y configuración tipada.

📖 Read the Full Documentation - Comprehensive guides, API reference, and examples

🚀 Características

  • Arquitectura Schema-per-Tenant: Cada tenant tiene su propio esquema de base de datos
  • Soporte Multi-ORM: Compatible con TypeORM y Drizzle ORM
  • Migraciones Automáticas: Migraciones internas para módulo admin (solo Drizzle)
  • Resolución Automática de Tenants: Soporte para múltiples estrategias (header, subdomain, JWT, custom)
  • Pool de Conexiones Dinámico: Gestión eficiente de conexiones por tenant
  • Inyección de Repositorios: Decoradores para inyectar repositorios específicos del tenant
  • Administración de Tenants: Módulo completo para CRUD de tenants
  • Registro de Entidades: Sistema flexible para configurar entidades por tenant
  • TypeScript: Completamente tipado con soporte completo de TypeScript
  • Escalable: Diseñado para aplicaciones de gran escala

Instalación

Instala el paquete y sus peer dependencies requeridas:

pnpm add nestjs-multitenant

Dependencias Peer

Para TypeORM (opción tradicional):

npm install @nestjs/common @nestjs/core @nestjs/typeorm @nestjs/config typeorm pg

Para Drizzle ORM (recomendado para mejor control de migraciones):

npm install @nestjs/common @nestjs/core @nestjs/config drizzle-orm pg
npm install -D drizzle-kit

Requisitos: Node.js >= 22, TypeScript >= 5.9, NestJS 11

📌 Importante: Drizzle ORM es recomendado para multi-tenancy avanzado. Ver Migraciones con Drizzle para más detalles.

🛠️ Configuración Básica

1. Configurar el Módulo Principal (forRoot)

Importa el módulo en tu aplicación e inicializa la configuración:

// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MultiTenantModule } from 'nestjs-multitenant';

@Module({
  imports: [
    // ⚠️ IMPORTANTE: ConfigModule debe importarse ANTES que MultiTenantModule
    ConfigModule.forRoot({
      isGlobal: true, // Hace que ConfigService esté disponible globalmente
    }),

    // Configuración de la base de datos principal
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'postgres',
      password: 'password',
      database: 'multitenant_db',
      schema: 'public',
      synchronize: true,
    }),

    // Configuración del módulo multi-tenant
    MultiTenantModule.forRoot({
      database: {
        host: 'localhost',
        port: 5432,
        username: 'postgres',
        password: 'password',
        database: 'multitenant_db',
      },
      autoCreateSchemas: true,
      enableAdminModule: true, // Habilita el módulo de administración
      platform: 'express', // express o fastify
      customControllers: [CustomTenantAdminController], // Permite cargar tu propia implementacion del controller, omitiendo el controller administrativo, si no se especifica tomara la implementacion propia del modulo interno
      customProviders: [
        createTenantStrategyProvider(TenantAdminService), // Type-safe
      ],
    }),
  ],
})
export class AppModule {}

Nota importante sobre forRoot:

  • La propiedad enableAdminModule, precargara la clase TenantAdminService y solo si la propiedad customControllers se encuentra vacia cargara TenantAdminController; esto brinda la ventaja de especificar tu propio controller.
  • Puedes omitir la clase precargada TenantAdminService, haciendo uso de la propiedad customProviders y especificar tu propia implementación.

2. Configuración Asíncrona (forRootAsync) - v2.1.0+

Para configuraciones dinámicas que requieren inyección de dependencias:

:::note Simplified Configuration (v2.1.0+) El método buildAsyncConfig() simplifica drásticamente la configuración asíncrona. Ver ejemplos completos en Configuración Avanzada. :::

Configuración recomendada con controller admin por defecto:

// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
  MultiTenantModule,
  createDatabaseConfigFromEnv,
} from 'nestjs-multitenant';

@Module({
  imports: [
    // ⚠️ IMPORTANTE: ConfigModule debe importarse ANTES que MultiTenantModule
    ConfigModule.forRoot({ isGlobal: true }),

    // Conexión principal de la aplicación
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'postgres',
      password: 'postgres',
      database: 'multitenant_db',
      schema: 'public',
      synchronize: true,
    }),

    // 🆕 Configuración simplificada con buildAsyncConfig (v2.1.0+)
    MultiTenantModule.forRootAsync(
      MultiTenantModule.buildAsyncConfig({
        ormType: 'typeorm',
        enableAdminController: true, // Incluye controller admin por defecto
        useFactory: (config: ConfigService) => ({
          database: createDatabaseConfigFromEnv(config),
          validationStrategy: 'local',
          autoCreateSchemas: true,
          platform: 'fastify',
        }),
        inject: [ConfigService],
      }),
    ),
  ],
})
export class AppModule {}

Configuración con controller personalizado:

// app.module.ts
MultiTenantModule.forRootAsync(
  MultiTenantModule.buildAsyncConfig({
    ormType: 'typeorm',
    enableAdminController: false, // No incluye controller por defecto
    additionalImports: [CustomTenantAdminModule], // Importa el módulo personalizado con el controller
    managementStrategyProvider: createTenantStrategyProvider(
      CustomTenantAdminService,
    ),
    useFactory: (config: ConfigService) => ({
      database: createDatabaseConfigFromEnv(config),
      validationStrategy: 'local',
      autoCreateSchemas: true,
      platform: 'fastify',
    }),
    inject: [ConfigService],
  }),
);

Nota importante sobre buildAsyncConfig (v2.1.0+):

  • 🆕 Simplificado: Maneja automáticamente la configuración específica del ORM
  • 🆕 Type-Safe: createTenantControllerFactory() para validación en tiempo de compilación
  • 🆕 Flexible: Soporta TypeORM y Drizzle con la misma API
  • Legacy forRootAsync: Aún disponible pero marcado como deprecated
  • 📖 Documentación completa: Ver Configuración Avanzada para todos los escenarios

3. Registro de Entidades para forRootAsync

Cuando uses forRootAsync, es CRÍTICO registrar las entidades ANTES de que se inicialice el módulo:

// entities/index.ts - Crear este archivo PRIMERO
import { EntityRegistry } from 'nestjs-multitenant';
import { User } from './user.entity';
import { Product } from './product.entity';

// IMPORTANTE: Registrar INMEDIATAMENTE al importar
// Opción 1: API fluida (recomendada)
EntityRegistry.getInstance()
  .registerEntity('User', User)
  .registerEntity('Product', Product);

// Opción 2: Registrar múltiples entidades
export const entities = {
  User: User,
  Product: Product,
};
EntityRegistry.getInstance().registerEntities(entities);
// app.module.ts
import { Module } from '@nestjs/common';
import './entities'; // IMPORTAR PRIMERO para registrar entidades
import { MultiTenantModule } from 'nestjs-multitenant';

@Module({
  imports: [
    // ... otros módulos
    MultiTenantModule.forRootAsync({
      // ... configuración
    }),
  ],
})
export class AppModule {}

⚠️ Problema Común: Si registras las entidades después de la inicialización del módulo, getEntityRegistryConfig() retornará un objeto entities vacío.

✅ Solución: Siempre importa el archivo de registro de entidades ANTES que MultiTenantModule.forRootAsync.

🔍 Debug: Usa getEntityRegistryDebugInfo() para verificar el estado del registro:

import { getEntityRegistryDebugInfo } from 'nestjs-multitenant';

// En un endpoint o servicio
const debugInfo = getEntityRegistryDebugInfo();
console.log('Registry state:', debugInfo);
// Output: { entityCount: 2, entities: ['User', 'Product'], presets: ['basic', 'full'] }

4. Variables de Entorno

# .env
DB_TYPE=postgres
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=password
DB_DATABASE=multitenant_db
TENANT_HEADER=x-tenant-id
AUTO_CREATE_SCHEMAS=true
ENABLE_ADMIN_MODULE=true
MULTITENANT_RUN_ADMIN_MIGRATIONS=true  # Para Drizzle (default: true)

📌 Nota: MULTITENANT_RUN_ADMIN_MIGRATIONS controla las migraciones automáticas del módulo admin (solo para Drizzle ORM).

🏗️ Uso del Sistema

Definir Entidades

Para TypeORM:

// entities/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  name: string;

  @Column({ unique: true })
  email: string;

  @Column()
  createdAt: Date;
}

Para Drizzle ORM:

// entities/user.entity.ts
import { pgTable, uuid, varchar, timestamp } from 'drizzle-orm/pg-core';
import { prefixSchema } from 'nestjs-multitenant';

// Usar el patrón de schemas recomendado
export const userSchema = prefixSchema('tenant_example');

export const users = userSchema.table('users', {
  id: uuid('id').defaultRandom().primaryKey(),
  name: varchar('name', { length: 255 }).notNull(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  createdAt: timestamp('created_at').defaultNow(),
});

📖 Drizzle Setup: Para guía completa de configuración de Drizzle, ver Setup PostgreSQL

Registrar Entidades

// entities/index.ts
import { EntityRegistry } from 'nestjs-multitenant';
import { User } from './user.entity';
import { Product } from './product.entity';

// Opción 1: Registrar entidades individualmente (API fluida)
EntityRegistry.getInstance()
  .registerEntity('User', User)
  .registerEntity('Product', Product)
  .registerPreset('basic', ['User'])
  .registerPreset('ecommerce', ['User', 'Product']);

// Opción 2: Registrar múltiples entidades a la vez
const entities = {
  User: User,
  Product: Product,
};

EntityRegistry.getInstance()
  .registerEntities(entities)
  .registerPresets({
    basic: ['User'],
    ecommerce: ['User', 'Product'],
  });

Crear un Servicio con Repositorios de Tenant

// services/user.service.ts
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectTenantRepository } from 'nestjs-multitenant';
import { User } from '../entities/user.entity';

@Injectable()
export class UserService {
  constructor(
    @InjectTenantRepository(User)
    private readonly userRepository: Repository<User>,
  ) {}

  async findAll(): Promise<User[]> {
    return this.userRepository.find();
  }

  async create(userData: Partial<User>): Promise<User> {
    const user = this.userRepository.create(userData);
    return this.userRepository.save(user);
  }

  async findById(id: string): Promise<User> {
    return this.userRepository.findOne({ where: { id } });
  }
}

Configurar un Módulo de Funcionalidad

// modules/user.module.ts
import { Module } from '@nestjs/common';
import { createTenantRepositoryProviders } from 'nestjs-multitenant';
import { User } from '../entities/user.entity';
import { UserService } from '../services/user.service';
import { UserController } from '../controllers/user.controller';

@Module({
  providers: [...createTenantRepositoryProviders([User]), UserService],
  controllers: [UserController],
  exports: [UserService],
})
export class UserModule {}

🔧 Configuración Avanzada

Estrategias de Resolución de Tenants

  • Header: Extrae el tenant ID de un encabezado HTTP.
  • Subdominio: Utiliza el subdominio como tenant ID.
  • Dominio: Extrae el tenant ID del dominio.
  • Query Parameter: Obtiene el tenant ID de un parámetro de consulta.
MultiTenantModule.forRootAsync({
  tenantResolution: {
    strategy: 'header' | 'subdomain' | 'jwt' | 'custom',
    headerName: 'x-tenant-id', // Encabezado por defecto
    jwtClaimName: 'tenantId', // Claim por defecto en JWT
    customResolver: (request: unknown) => {
      // Lógica personalizada para resolver tenant
      return 'default-tenant';
    },
  },
});

Nota Importante:

  • Si la estrategia de resolución es header, asegúrate de que el encabezado exista en la solicitud y definir headerName si es diferente de x-tenant-id.
  • Si la estrategia de resolución es subdomain, asegúrate de que el subdominio esté configurado correctamente.
  • Si la estrategia de resolución es jwt, asegúrate de que el token JWT esté presente y válido; ademas, definir jwtClaimName si es diferente de tenantId.
  • Si la estrategia de resolución es custom, asegúrate de proporcionar una función personalizada que devuelva el tenant ID basado en la solicitud.

Pool de Conexiones

MultiTenantModule.forRoot({
  // ... otras configuraciones
  connectionPool: {
    maxConnections: 50,
    idleTimeout: 30000,
    enableCleanup: 30000,
    cleanupInterval: 5000,
  },
});

Configuración de Entidades por Tenant

// Configurar entidades disponibles por tenant
EntityRegistry.getInstance()
  .registerPreset('startup', ['User', 'Project'])
  .registerPreset('enterprise', ['User', 'Project', 'Analytics', 'Billing']);

🔍 Uso Avanzado

Acceso Directo al DataSource del Tenant

import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { InjectTenantDataSource } from 'nestjs-multitenant';

@Injectable()
export class AdvancedService {
  constructor(
    @InjectTenantDataSource()
    private readonly dataSource: DataSource,
  ) {}

  async executeRawQuery(query: string) {
    return this.dataSource.query(query);
  }

  async runTransaction(callback: (manager: EntityManager) => Promise<any>) {
    return this.dataSource.transaction(callback);
  }
}

Factory de Repositorios

import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectTenantRepositoryFactory } from 'nestjs-multitenant';
import { User } from '../entities/user.entity';

@Injectable()
export class MultiTenantService {
  constructor(
    @InjectTenantRepositoryFactory(User)
    private readonly userRepositoryFactory: (
      tenantId: string,
    ) => Promise<Repository<User>>,
  ) {}

  async getUsersFromSpecificTenant(tenantId: string) {
    const userRepository = await this.userRepositoryFactory(tenantId);
    return userRepository.find();
  }
}

🔄 Migraciones con Drizzle ORM

Para proyectos con Drizzle ORM, el módulo incluye capacidades avanzadas de migración:

Configuración Automática

El módulo gestiona automáticamente las migraciones del schema de administración:

// drizzle.config.ts
import type { Config } from 'drizzle-kit';

export default {
  schema: './src/entities/**/*.ts',
  out: './drizzle',
  driver: 'pg',
  dbCredentials: {
    url: process.env.DATABASE_URL,
  },
} satisfies Config;

Flujo de Trabajo

  1. Generar migración: drizzle-kit generate
  2. Aplicar migración: drizzle-kit migrate
  3. Migraciones admin: Automáticas controladas por MULTITENANT_RUN_ADMIN_MIGRATIONS

📖 Guía Completa: Ver Migraciones con Drizzle para detalles completos.

Problemas Comunes

Errores de Migración con Drizzle

Problema: Las migraciones de tenant no se aplican correctamente.

Solución: Asegúrate de seguir el patrón prefixSchema:

const prefixSchema = (schema: string) => pgSchema(`tenant_${schema}`);
export const tenantSchema = prefixSchema('your-tenant-id');

Problema: Migraciones del módulo admin no se ejecutan.

Solución: Verifica la variable de entorno:

MULTITENANT_RUN_ADMIN_MIGRATIONS=true

Conexión a Base de Datos

Problema: Errores de conexión o timeout.

Solución: Verifica la configuración de la base de datos y asegúrate de que PostgreSQL esté ejecutándose:

MultiTenantModule.forRoot({
  database: {
    host: 'localhost',
    port: 5432,
    username: 'postgres',
    password: 'password',
    database: 'multitenant_db',
    // Opcional: configuración adicional
    ssl: false,
    synchronize: true, // Solo en desarrollo
    logging: true, // Para debug
  },
});

🔧 Resolución de Problemas

Error: "Nest can't resolve dependencies of the TypeOrmModuleOptions"

Problema: Error al inicializar el módulo con el mensaje:

UnknownDependenciesException [Error]: Nest can't resolve dependencies of the TypeOrmModuleOptions (?).
Please make sure that the argument ConfigService at index [0] is available in the TypeOrmCoreModule context.

Solución: Este error ocurre cuando ConfigModule no está disponible en el contexto del módulo. Asegúrate de:

  1. Importar ConfigModule ANTES que MultiTenantModule:
@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }), // ⚠️ DEBE ir ANTES
    MultiTenantModule.forRoot({ /* config */ }),
  ],
})
  1. Usar configuración global:
ConfigModule.forRoot({
  isGlobal: true, // Hace ConfigService disponible globalmente
});
  1. Para configuración asíncrona, inyectar ConfigService correctamente:
MultiTenantModule.forRootAsync({
  inject: [ConfigService],
  useFactory: (configService: ConfigService) => ({
    // configuración
  }),
});

Error: "Cannot find module 'nestjs-multitenant'"

Solución: Instala las dependencias requeridas:

Para TypeORM:

npm install @nestjs/common @nestjs/core @nestjs/typeorm @nestjs/config typeorm pg

Para Drizzle ORM:

npm install @nestjs/common @nestjs/core @nestjs/config drizzle-orm pg
npm install -D drizzle-kit

Problemas de Conexión a Base de Datos

Problema: Errores de conexión o timeout.

Solución: Verifica la configuración de la base de datos y asegúrate de que PostgreSQL esté ejecutándose:

MultiTenantModule.forRoot({
  database: {
    host: 'localhost',
    port: 5432,
    username: 'postgres',
    password: 'password',
    database: 'multitenant_db',
    // Opcional: configuración adicional
    ssl: false,
    synchronize: true, // Solo en desarrollo
    logging: true, // Para debug
  },
});

🤝 Contribución

  1. Fork el proyecto
  2. Crea una rama para tu feature (git checkout -b feature/amazing-feature)
  3. Commit tus cambios (git commit -m 'Add amazing feature')
  4. Push a la rama (git push origin feature/amazing-feature)
  5. Abre un Pull Request

Versionado y CHANGELOG

Este proyecto sigue SemVer. Las releases se realizan con mensajes de commit semánticos y se documentan en CHANGELOG.md.

🆘 Soporte

Licencia

MIT © Reymi Tech