nest-multitenant
v1.0.0
Published
Enterprise-grade multi-tenancy middleware for NestJS applications with support for multiple tenant resolution strategies and database isolation patterns
Maintainers
Readme
nest-multitenant
Enterprise-grade multi-tenancy middleware for NestJS applications with support for multiple tenant resolution strategies and database isolation patterns.
🚀 Features
Multiple Tenant Resolution Strategies
- Subdomain-based (
tenant1.example.com) - Header-based (
X-Tenant-ID) - Path-based (
/tenant1/api/users) - Custom resolver support
- Subdomain-based (
Database Isolation Patterns
- Shared database with tenant_id column
- Schema per tenant
- Database per tenant
Production-Ready
- TypeScript with strict typing
- Comprehensive test coverage (>80%)
- Request-scoped tenant context
- Tenant validation and guards
- Graceful error handling
Developer-Friendly
- Easy integration with TypeORM and Prisma
- Decorators for accessing tenant context
- Configurable ignored paths
- Caching support
📦 Installation
npm install nest-multitenantPeer Dependencies:
npm install @nestjs/common @nestjs/core reflect-metadata rxjs🔧 Quick Start
1. Implement TenantStore
Create a service that implements the TenantStore interface:
import { Injectable } from '@nestjs/common';
import { TenantStore, Tenant } from 'nest-multitenant';
@Injectable()
export class DatabaseTenantStore implements TenantStore {
async findById(id: string): Promise<Tenant | null> {
// Fetch tenant from your database
return {
id: 'tenant1',
name: 'Tenant One',
subdomain: 'tenant1',
isActive: true,
};
}
async findBySubdomain(subdomain: string): Promise<Tenant | null> {
// Fetch tenant by subdomain
return null;
}
async findByCustomKey(key: string, value: string): Promise<Tenant | null> {
return null;
}
async findAll(): Promise<Tenant[]> {
return [];
}
}2. Register the Module
import { Module } from '@nestjs/common';
import { MultitenantModule, TenantResolutionStrategy } from 'nest-multitenant';
import { DatabaseTenantStore } from './tenant.store';
@Module({
imports: [
MultitenantModule.forRoot({
tenantResolutionStrategy: TenantResolutionStrategy.HEADER,
tenantStore: DatabaseTenantStore,
headerName: 'X-Tenant-ID',
throwOnMissingTenant: true,
ignoredPaths: ['/health', '/metrics'],
}),
],
})
export class AppModule {}3. Use Tenant Context in Controllers
import { Controller, Get } from '@nestjs/common';
import { CurrentTenant, Tenant } from 'nest-multitenant';
@Controller('users')
export class UsersController {
@Get()
async getUsers(@CurrentTenant() tenant: Tenant) {
return `Fetching users for tenant: ${tenant.name}`;
}
}📖 Configuration Options
Module Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| tenantResolutionStrategy | TenantResolutionStrategy | Required | How to resolve the tenant from requests |
| tenantStore | Type<TenantStore> | Required | Implementation of tenant data store |
| headerName | string | 'X-Tenant-ID' | Header name for header-based resolution |
| pathPrefix | string | - | Path prefix for path-based resolution |
| customResolver | Type<TenantResolver> | - | Custom resolver (required for CUSTOM strategy) |
| throwOnMissingTenant | boolean | true | Throw exception if tenant cannot be resolved |
| defaultTenantId | string | - | Default tenant when resolution fails |
| enableCaching | boolean | false | Enable tenant data caching |
| cacheTTL | number | 3600 | Cache TTL in seconds |
| ignoredPaths | string[] | [] | Paths to skip tenant resolution |
🎯 Resolution Strategies
Subdomain Strategy
Resolves tenants from subdomain:
MultitenantModule.forRoot({
tenantResolutionStrategy: TenantResolutionStrategy.SUBDOMAIN,
tenantStore: DatabaseTenantStore,
});
// tenant1.example.com -> resolves to tenant with subdomain='tenant1'Header Strategy
Resolves tenants from HTTP header:
MultitenantModule.forRoot({
tenantResolutionStrategy: TenantResolutionStrategy.HEADER,
headerName: 'X-Tenant-ID',
tenantStore: DatabaseTenantStore,
});
// Request with header: X-Tenant-ID: tenant1Path Strategy
Resolves tenants from URL path:
MultitenantModule.forRoot({
tenantResolutionStrategy: TenantResolutionStrategy.PATH,
tenantStore: DatabaseTenantStore,
});
// /tenant1/api/users -> resolves to tenant with id='tenant1'Custom Strategy
Implement your own resolution logic:
@Injectable()
export class CustomTenantResolver implements TenantResolver {
constructor(@Inject(TENANT_STORE) private tenantStore: TenantStore) {}
async resolve(request: Request): Promise<Tenant | null> {
const apiKey = request.headers['x-api-key'];
return this.tenantStore.findByCustomKey('apiKey', apiKey);
}
}
MultitenantModule.forRoot({
tenantResolutionStrategy: TenantResolutionStrategy.CUSTOM,
customResolver: CustomTenantResolver,
tenantStore: DatabaseTenantStore,
});🛡️ Guards and Decorators
TenantGuard
Ensure a tenant has been resolved:
import { Controller, Get, UseGuards } from '@nestjs/common';
import { TenantGuard } from 'nest-multitenant';
@Controller('protected')
@UseGuards(TenantGuard)
export class ProtectedController {
// All routes require a valid tenant
}Decorators
import { CurrentTenant, TenantCtx, TenantProperty } from 'nest-multitenant';
@Get()
async example1(@CurrentTenant() tenant: Tenant) {
// Access full tenant object
}
@Get()
async example2(@TenantCtx() context: TenantContext) {
// Access tenant context with metadata
console.log(context.source); // 'header', 'subdomain', etc.
}
@Get()
async example3(@TenantProperty('id') tenantId: string) {
// Access specific tenant property
}🗄️ Database Integration
TypeORM Example
import { Injectable, Scope } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
@Injectable({ scope: Scope.REQUEST })
export class UserService {
private tenantId: string;
constructor(
@Inject(REQUEST) request: Request,
@InjectRepository(User) private userRepo: Repository<User>,
) {
this.tenantId = request.tenantContext?.tenant.id || '';
}
async findAll() {
return this.userRepo.find({ where: { tenantId: this.tenantId } });
}
}Prisma Example
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Injectable()
export class UserService {
constructor(private prisma: PrismaService) {}
async findAll(tenantId: string) {
return this.prisma.user.findMany({
where: { tenantId },
});
}
}🔒 Security Considerations
- Always validate tenant IDs to prevent tenant isolation bypass
- Use HTTPS in production for header-based resolution
- Implement rate limiting per tenant
- Audit tenant switching actions
- Consider row-level security in your database
🧪 Testing
Run tests:
npm testWith coverage:
npm run test:cov📝 Example Application
See the examples directory for complete working examples with:
- Docker setup
- TypeORM integration
- API examples
- Testing strategies
🤝 Contributing
Contributions are welcome! Please read our Contributing Guide for details.
📄 License
MIT License - see LICENSE file for details.
👤 Author
Alireza Aminzadeh
- Email: [email protected]
- GitHub: @syeedalireza
- NPM: @syeedalireza
⭐ Support
If this package helped you, please give it a ⭐ on GitHub!
🔗 Related Packages
Built with ❤️ for the NestJS community
