@lexmata/nestjs-multi-tenant
v0.1.1
Published
A NestJS module for building multi-tenant applications
Maintainers
Readme
@lexmata/nestjs-multi-tenant
A flexible NestJS module for building multi-tenant applications. Supports multiple tenant identification strategies and provides seamless tenant context management throughout your application.
Features
- 🔌 Multiple extraction strategies - Header, subdomain, path, query parameter, or custom
- 🧵 AsyncLocalStorage context - Access tenant info anywhere without prop drilling
- 🔒 Guards & decorators - Declarative tenant requirements
- ⚡ Async configuration - Load config from external sources
- 🎯 Route exclusions - Skip tenant extraction for specific routes
- 📦 Zero dependencies - Only requires NestJS peer dependencies
Installation
# npm
npm install @lexmata/nestjs-multi-tenant
# yarn
yarn add @lexmata/nestjs-multi-tenant
# pnpm
pnpm add @lexmata/nestjs-multi-tenantPeer Dependencies
This package requires the following peer dependencies:
{
"@nestjs/common": "^10.0.0 || ^11.0.0",
"@nestjs/core": "^10.0.0 || ^11.0.0",
"reflect-metadata": "^0.1.13 || ^0.2.0",
"rxjs": "^7.0.0"
}Quick Start
1. Import the module
import { Module } from '@nestjs/common';
import { MultiTenantModule } from '@lexmata/nestjs-multi-tenant';
@Module({
imports: [
MultiTenantModule.forRoot({
extractionStrategy: 'header',
tenantHeader: 'x-tenant-id',
}),
],
})
export class AppModule {}2. Use in your controllers
import { Controller, Get } from '@nestjs/common';
import { CurrentTenant, TenantId, Tenant } from '@lexmata/nestjs-multi-tenant';
@Controller('users')
export class UsersController {
@Get()
findAll(@CurrentTenant() tenant: Tenant) {
console.log(`Fetching users for tenant: ${tenant.id}`);
return this.usersService.findAll(tenant.id);
}
@Get('profile')
getProfile(@TenantId() tenantId: string) {
return this.usersService.getProfile(tenantId);
}
}3. Access tenant anywhere with TenantContextService
import { Injectable } from '@nestjs/common';
import { TenantContextService } from '@lexmata/nestjs-multi-tenant';
@Injectable()
export class UsersService {
constructor(private readonly tenantContext: TenantContextService) {}
findAll() {
const tenantId = this.tenantContext.getTenantId();
// Use tenantId for database queries, etc.
}
}Configuration Options
Basic Configuration
MultiTenantModule.forRoot({
// Extraction strategy (default: 'header')
extractionStrategy: 'header' | 'subdomain' | 'path' | 'query' | 'cookie' | 'jwt' | 'bearer' | 'custom',
// Header name for 'header' strategy (default: 'x-tenant-id')
tenantHeader: 'x-tenant-id',
// Query param for 'query' strategy (default: 'tenantId')
tenantQueryParam: 'tenantId',
// Path segment index for 'path' strategy (default: 0)
tenantPathIndex: 0,
// Cookie name for 'cookie' strategy (default: 'tenant_id')
tenantCookie: 'tenant_id',
// JWT claim path for 'jwt' strategy (default: 'tenantId')
// Supports dot notation for nested claims (e.g., 'user.tenantId')
jwtTenantClaim: 'tenantId',
// Function to resolve tenant ID from bearer token (for 'bearer' strategy)
bearerTokenResolver: async (token) => {
const apiKey = await apiKeyService.findByKey(token);
return apiKey?.tenantId ?? null;
},
// Cache configuration for tenant resolver (reduces database lookups)
tenantResolverCache: {
enabled: true, // Enable caching
ttl: 300_000, // 5 minutes (default)
max: 1000, // Max entries (default)
},
// Event hooks for logging, metrics, and custom logic
eventHooks: {
onTenantIdExtracted: (tenantId, ctx) => {
logger.debug(`Tenant ID extracted: ${tenantId} from ${ctx.strategy}`);
},
onTenantResolved: (tenant, ctx) => {
metrics.increment('tenant.resolved', { tenant: tenant.id });
},
onTenantNotFound: (tenantId, ctx) => {
logger.warn(`Tenant not found: ${tenantId}`);
},
onTenantMissing: (ctx) => {
logger.debug(`No tenant in request: ${ctx.path}`);
},
},
// Custom extractor function for 'custom' strategy
customExtractor: (request) => request.headers['x-custom-header'],
// Resolve full tenant data from ID
tenantResolver: async (tenantId) => {
return { id: tenantId, name: 'Tenant Name', plan: 'premium' };
},
// Throw error if tenant cannot be determined (default: false)
requireTenant: false,
// Routes to exclude from tenant extraction
excludeRoutes: ['/health', '/api/public', /^\/docs/],
})Async Configuration
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
MultiTenantModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
extractionStrategy: config.get('TENANT_STRATEGY'),
tenantHeader: config.get('TENANT_HEADER'),
requireTenant: config.get('REQUIRE_TENANT'),
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}Extraction Strategies
Header Strategy (Default)
Extract tenant ID from a request header.
MultiTenantModule.forRoot({
extractionStrategy: 'header',
tenantHeader: 'x-tenant-id', // default
})curl -H "x-tenant-id: tenant-123" http://localhost:3000/api/usersSubdomain Strategy
Extract tenant ID from the subdomain.
MultiTenantModule.forRoot({
extractionStrategy: 'subdomain',
})tenant-123.example.com → tenant ID: "tenant-123"Path Strategy
Extract tenant ID from a URL path segment.
MultiTenantModule.forRoot({
extractionStrategy: 'path',
tenantPathIndex: 0, // First path segment after /
})/tenant-123/api/users → tenant ID: "tenant-123"
/api/tenant-123/users → tenant ID: "tenant-123" (with tenantPathIndex: 1)Query Strategy
Extract tenant ID from a query parameter.
MultiTenantModule.forRoot({
extractionStrategy: 'query',
tenantQueryParam: 'tenantId', // default
})/api/users?tenantId=tenant-123 → tenant ID: "tenant-123"Cookie Strategy
Extract tenant ID from a cookie.
MultiTenantModule.forRoot({
extractionStrategy: 'cookie',
tenantCookie: 'tenant_id', // default
})Cookie: tenant_id=tenant-123 → tenant ID: "tenant-123"Note: Works with or without
cookie-parsermiddleware. Ifcookie-parseris not used, cookies are parsed from theCookieheader automatically.
JWT Strategy
Extract tenant ID from a JWT token in the Authorization header.
MultiTenantModule.forRoot({
extractionStrategy: 'jwt',
jwtTenantClaim: 'tenantId', // default
})Authorization: Bearer eyJhbGc... → Decodes JWT, extracts claim "tenantId"Supports nested claims with dot notation:
MultiTenantModule.forRoot({
extractionStrategy: 'jwt',
jwtTenantClaim: 'user.organization.id', // nested path
})// JWT Payload:
{
"user": {
"organization": {
"id": "tenant-123" // ← extracted
}
}
}Note: The JWT is decoded but not verified. Token verification should be handled by your authentication guards (e.g.,
@nestjs/passport,@nestjs/jwt). This strategy trusts that tokens have already been validated.
Bearer Token Strategy
Extract tenant ID from opaque bearer tokens (like API keys) using a resolver function.
MultiTenantModule.forRoot({
extractionStrategy: 'bearer',
bearerTokenResolver: async (token) => {
// Look up API key in database to get tenant ID
const apiKey = await this.apiKeyService.findByKey(token);
return apiKey?.tenantId ?? null;
},
})Authorization: Bearer sk_live_abc123 → Calls resolver with "sk_live_abc123"Use cases:
- API key authentication where keys are mapped to tenants
- OAuth2 opaque access tokens
- Session tokens that require database lookup
Note: Unlike
jwtstrategy which decodes the token,bearerstrategy passes the raw token to your resolver function. This is ideal for opaque tokens that require external lookup.
Custom Strategy
Implement your own extraction logic.
MultiTenantModule.forRoot({
extractionStrategy: 'custom',
customExtractor: async (request) => {
// Extract from JWT token
const token = request.headers.authorization?.replace('Bearer ', '');
if (token) {
const decoded = jwt.decode(token);
return decoded?.tenantId || null;
}
return null;
},
})Tenant Resolution
Enrich tenant data by providing a resolver function:
MultiTenantModule.forRoot({
extractionStrategy: 'header',
tenantResolver: async (tenantId: string) => {
// Fetch from database
const tenant = await this.tenantsRepository.findOne(tenantId);
if (!tenant) return null;
return {
id: tenant.id,
name: tenant.name,
plan: tenant.subscriptionPlan,
settings: tenant.settings,
};
},
})Tenant Resolver Caching
Reduce database lookups by caching resolved tenant data:
MultiTenantModule.forRoot({
extractionStrategy: 'header',
tenantResolver: async (tenantId) => {
return await this.tenantsRepository.findOne(tenantId);
},
tenantResolverCache: {
enabled: true,
ttl: 300_000, // 5 minutes (default)
max: 1000, // Max cached tenants (default)
},
})Cache Management
The middleware exposes methods for cache management:
@Injectable()
export class TenantService {
constructor(
@Inject(TenantMiddleware)
private readonly tenantMiddleware: TenantMiddleware,
) {}
// Get cache statistics
getStats() {
return this.tenantMiddleware.getCacheStats();
// { enabled: true, size: 42, max: 1000, ttl: 300000 }
}
// Invalidate a specific tenant (e.g., after update)
onTenantUpdate(tenantId: string) {
this.tenantMiddleware.invalidateTenant(tenantId);
}
// Clear entire cache
clearAllCache() {
this.tenantMiddleware.clearCache();
}
}When to Use Caching
| Scenario | Recommendation | |----------|----------------| | High-traffic APIs | ✅ Enable with short TTL (1-5 min) | | Tenant data rarely changes | ✅ Enable with longer TTL (10-30 min) | | Real-time tenant updates needed | ❌ Disable or use very short TTL | | Low-traffic internal APIs | ❌ Usually not needed |
Event Hooks
React to tenant lifecycle events for logging, metrics, or custom logic:
MultiTenantModule.forRoot({
extractionStrategy: 'header',
tenantResolver: (id) => this.tenantService.findById(id),
eventHooks: {
// Called when tenant ID is extracted from request
onTenantIdExtracted: (tenantId, context) => {
console.log(`Extracted tenant: ${tenantId} via ${context.strategy}`);
},
// Called when tenant is successfully resolved
onTenantResolved: (tenant, context) => {
metrics.increment('tenant.resolved', { plan: tenant.plan });
},
// Called when resolver returns null
onTenantNotFound: (tenantId, context) => {
logger.warn(`Unknown tenant: ${tenantId} at ${context.path}`);
},
// Called when no tenant ID in request
onTenantMissing: (context) => {
logger.debug(`Anonymous request: ${context.path}`);
},
},
})Event Context
All hooks receive a context object:
interface TenantEventContext {
request: unknown; // The HTTP request object
strategy: TenantExtractionStrategy; // 'header', 'jwt', etc.
path: string; // Request path
}Use Cases
| Hook | Use Case |
|------|----------|
| onTenantIdExtracted | Audit logging, request tracing |
| onTenantResolved | Metrics, feature flags per tenant |
| onTenantNotFound | Security alerts, invalid tenant monitoring |
| onTenantMissing | Analytics for anonymous traffic |
| onTenantValidationFailed | Security alerts, access denied logging |
Tenant Validation
Validate tenants before allowing requests. Useful for checking subscription status, permissions, or tenant state:
MultiTenantModule.forRoot({
extractionStrategy: 'header',
tenantResolver: (id) => this.tenantService.findById(id),
requireTenant: true,
// Simple boolean validation
tenantValidator: (tenant) => tenant.isActive === true,
// Async validation with database check
tenantValidator: async (tenant, ctx) => {
const subscription = await this.subscriptionService.check(tenant.id);
return subscription.status === 'active';
},
// Validation with custom error message
tenantValidator: (tenant) => {
if (!tenant.isActive) {
return { valid: false, reason: 'Tenant is deactivated' };
}
if (tenant.subscriptionExpired) {
return { valid: false, reason: 'Subscription expired' };
}
return { valid: true };
},
})Validation Result
Return a boolean or TenantValidationResult:
interface TenantValidationResult {
valid: boolean;
reason?: string; // Custom error message (default: "Tenant validation failed")
}Validation Flow
Request → Extract ID → Resolve Tenant → Validate → Set Context → Handle Request
↓
If invalid & requireTenant: HTTP 403 Forbidden
If invalid & !requireTenant: Continue without tenantUse Cases
| Scenario | Implementation |
|----------|---------------|
| Active tenant check | (t) => t.isActive |
| Subscription validation | (t) => t.subscriptionStatus === 'active' |
| Feature flag check | (t, ctx) => hasFeature(t, ctx.path) |
| Rate limiting | async (t) => await checkRateLimit(t.id) |
| IP allowlist | (t, ctx) => t.allowedIps.includes(getIp(ctx.request)) |
Debug Mode
Enable detailed logging to understand what's happening during tenant extraction:
MultiTenantModule.forRoot({
extractionStrategy: 'header',
tenantResolver: (id) => this.tenantService.findById(id),
debug: true, // Enable debug logging
})Sample Output
[MultiTenant] MultiTenant middleware initialized
[MultiTenant] Strategy: header
[MultiTenant] Require tenant: false
[MultiTenant] Cache enabled: true
[MultiTenant] Cache TTL: 300000ms
[MultiTenant] Cache max: 1000
[MultiTenant] [GET /api/users] Extracting tenant using 'header' strategy
[MultiTenant] [GET /api/users] Tenant ID extracted: tenant-123
[MultiTenant] [GET /api/users] Resolving tenant data for: tenant-123
[MultiTenant] [GET /api/users] Cache miss for tenant: tenant-123
[MultiTenant] [GET /api/users] Tenant resolved: tenant-123 (Acme Corp)
[MultiTenant] [GET /api/users] Cached tenant: tenant-123 (expires in 300000ms)
[MultiTenant] [GET /api/users] Validating tenant: tenant-123
[MultiTenant] [GET /api/users] Validation passed
[MultiTenant] [GET /api/users] Setting tenant context: tenant-123When to Use Debug Mode
| Environment | Recommendation | |-------------|----------------| | Development | ✅ Enable for troubleshooting | | Testing | ✅ Enable to verify tenant flow | | Staging | ⚠️ Enable temporarily for debugging | | Production | ❌ Disable (performance overhead) |
Decorators
All decorators work with both REST controllers and GraphQL resolvers.
@CurrentTenant()
Inject the full tenant object into a controller method or resolver.
// REST Controller
@Get()
findAll(@CurrentTenant() tenant: Tenant) {
// tenant: { id: 'tenant-123', name: 'Acme Corp', ... }
}
// GraphQL Resolver
@Query(() => [User])
users(@CurrentTenant() tenant: Tenant) {
return this.userService.findByTenant(tenant.id);
}@TenantId()
Inject only the tenant ID.
// REST Controller
@Get()
findAll(@TenantId() tenantId: string) {
// tenantId: 'tenant-123'
}
// GraphQL Resolver
@Query(() => User)
user(@TenantId() tenantId: string, @Args('id') id: string) {
return this.userService.findOne(tenantId, id);
}@RequireTenant()
Mark a controller, resolver, or method as requiring a valid tenant context. Use with TenantGuard.
import { Controller, Get, UseGuards } from '@nestjs/common';
import { RequireTenant, TenantGuard } from '@lexmata/nestjs-multi-tenant';
// Apply to entire controller
@Controller('users')
@UseGuards(TenantGuard)
@RequireTenant()
export class UsersController {
@Get()
findAll() {
// Guaranteed to have tenant context
}
}
// Apply to GraphQL resolver
@Resolver(() => User)
@UseGuards(TenantGuard)
@RequireTenant()
export class UsersResolver {
@Query(() => [User])
users() {
// Guaranteed to have tenant context
}
}
// Or apply to specific methods
@Controller('mixed')
@UseGuards(TenantGuard)
export class MixedController {
@Get('public')
publicEndpoint() {
// No tenant required
}
@Get('private')
@RequireTenant()
privateEndpoint() {
// Tenant required
}
}GraphQL Support
The module automatically detects GraphQL context and extracts tenant from the underlying HTTP request.
Setup
// app.module.ts
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { MultiTenantModule } from '@lexmata/nestjs-multi-tenant';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: true,
context: ({ req }) => ({ req }), // Important: pass request to context
}),
MultiTenantModule.forRoot({
extractionStrategy: 'header',
tenantResolver: (id) => this.tenantService.findById(id),
}),
],
})
export class AppModule {}Complete Resolver Example
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { CurrentTenant, TenantId, RequireTenant, TenantGuard } from '@lexmata/nestjs-multi-tenant';
import type { Tenant } from '@lexmata/nestjs-multi-tenant';
@Resolver(() => User)
@UseGuards(TenantGuard)
@RequireTenant()
export class UsersResolver {
constructor(private readonly usersService: UsersService) {}
@Query(() => [User])
async users(@CurrentTenant() tenant: Tenant) {
return this.usersService.findAllByTenant(tenant.id);
}
@Query(() => User, { nullable: true })
async user(@TenantId() tenantId: string, @Args('id') id: string) {
return this.usersService.findOne(tenantId, id);
}
@Mutation(() => User)
async createUser(
@CurrentTenant() tenant: Tenant,
@Args('input') input: CreateUserInput,
) {
return this.usersService.create(tenant.id, input);
}
}GraphQL Client Headers
Send tenant identification in HTTP headers with your GraphQL requests:
# Header: x-tenant-id: tenant-123
query {
users {
id
name
}
}WebSocket Support
All decorators and guards work with WebSocket gateways. The tenant is extracted from the WebSocket client object.
Setup
Set tenant on the WebSocket client during the connection handshake:
// chat.gateway.ts
import { WebSocketGateway, OnGatewayConnection, WebSocketServer } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { MultiTenantModule, TenantContextService } from '@lexmata/nestjs-multi-tenant';
@WebSocketGateway()
export class ChatGateway implements OnGatewayConnection {
@WebSocketServer()
server: Server;
constructor(
private readonly tenantService: TenantService,
private readonly tenantContext: TenantContextService,
) {}
async handleConnection(client: Socket) {
// Extract tenant ID from handshake (headers, query, or auth)
const tenantId = client.handshake.headers['x-tenant-id'] as string
|| client.handshake.query.tenantId as string;
if (tenantId) {
// Resolve and attach tenant to client
const tenant = await this.tenantService.findById(tenantId);
if (tenant) {
client.data.tenant = tenant; // or client.tenant = tenant
}
}
}
}Gateway with Decorators
import { WebSocketGateway, SubscribeMessage, MessageBody, ConnectedSocket } from '@nestjs/websockets';
import { UseGuards } from '@nestjs/common';
import { CurrentTenant, TenantId, RequireTenant, TenantGuard } from '@lexmata/nestjs-multi-tenant';
import type { Tenant } from '@lexmata/nestjs-multi-tenant';
import { Socket } from 'socket.io';
@WebSocketGateway()
@UseGuards(TenantGuard)
@RequireTenant()
export class ChatGateway {
constructor(private readonly chatService: ChatService) {}
@SubscribeMessage('message')
handleMessage(
@CurrentTenant() tenant: Tenant,
@MessageBody() data: { room: string; message: string },
@ConnectedSocket() client: Socket,
) {
return this.chatService.broadcastMessage(tenant.id, data.room, data.message);
}
@SubscribeMessage('join-room')
handleJoinRoom(
@TenantId() tenantId: string,
@MessageBody() roomId: string,
@ConnectedSocket() client: Socket,
) {
// Join tenant-specific room
client.join(`${tenantId}:${roomId}`);
return { event: 'joined', room: `${tenantId}:${roomId}` };
}
}Tenant Location on WebSocket Client
The decorator checks these locations in order:
client.tenant- Direct property on socketclient.handshake.tenant- Set during handshakeclient.data.tenant- Socket.io data property (recommended)
// Option 1: Direct property
client.tenant = tenant;
// Option 2: Handshake (read-only after connection)
// Set via middleware before connection
// Option 3: Data property (recommended for Socket.io)
client.data.tenant = tenant;Microservice Support
All decorators and guards work with NestJS microservices (TCP, Redis, RabbitMQ, Kafka, gRPC, etc.). The tenant is extracted from the message payload or RPC context.
Passing Tenant in Message Payload
Include tenant information in your message payload:
// Producer/Client side
this.client.send('user.create', {
tenantId: 'tenant-123', // Just the ID
// OR
tenant: { id: 'tenant-123', name: 'Acme Corp' }, // Full tenant object
payload: { email: '[email protected]', name: 'John' },
});
// Or with event pattern
this.client.emit('order.created', {
tenantId: 'tenant-123',
order: { id: 'order-456', total: 99.99 },
});Handler with Decorators
import { Controller } from '@nestjs/common';
import { MessagePattern, EventPattern, Payload } from '@nestjs/microservices';
import { UseGuards } from '@nestjs/common';
import { CurrentTenant, TenantId, RequireTenant, TenantGuard } from '@lexmata/nestjs-multi-tenant';
import type { Tenant } from '@lexmata/nestjs-multi-tenant';
@Controller()
@UseGuards(TenantGuard)
@RequireTenant()
export class UsersHandler {
constructor(private readonly usersService: UsersService) {}
@MessagePattern('user.create')
async createUser(
@CurrentTenant() tenant: Tenant,
@Payload() data: { payload: CreateUserDto },
) {
return this.usersService.create(tenant.id, data.payload);
}
@MessagePattern('user.findAll')
async findAllUsers(@TenantId() tenantId: string) {
return this.usersService.findAll(tenantId);
}
@EventPattern('order.created')
async handleOrderCreated(
@CurrentTenant() tenant: Tenant,
@Payload() data: { order: OrderDto },
) {
await this.notificationService.notifyOrderCreated(tenant.id, data.order);
}
}Using RPC Context
For more complex scenarios, you can set the tenant on the RPC context:
// Custom interceptor to set tenant on context
@Injectable()
export class TenantInterceptor implements NestInterceptor {
constructor(private readonly tenantService: TenantService) {}
async intercept(context: ExecutionContext, next: CallHandler) {
if (context.getType() === 'rpc') {
const rpcContext = context.switchToRpc();
const data = rpcContext.getData();
const ctx = rpcContext.getContext();
if (data.tenantId) {
const tenant = await this.tenantService.findById(data.tenantId);
ctx.tenant = tenant;
}
}
return next.handle();
}
}Tenant Location in Microservices
The decorator checks these locations in order:
data.tenant- Full tenant object in message payloaddata.tenantId- Tenant ID in payload (wrapped as{ id: tenantId })context.tenant- Set by interceptor or middlewarecontext.getTenant()- Custom getter function
Cross-Service Tenant Propagation
When calling other microservices, propagate the tenant context:
@Injectable()
export class OrderService {
constructor(
@Inject('BILLING_SERVICE') private billingClient: ClientProxy,
private readonly tenantContext: TenantContextService,
) {}
async createOrder(orderDto: CreateOrderDto) {
const tenantId = this.tenantContext.getTenantId();
// Include tenant in outgoing messages
return this.billingClient.send('billing.charge', {
tenantId,
order: orderDto,
});
}
}Fastify Support
This module works seamlessly with both Express and Fastify adapters. No additional configuration is required.
Using with Fastify
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
await app.listen(3000);
}
bootstrap();All extraction strategies work identically with Fastify:
// app.module.ts - same configuration for both Express and Fastify
@Module({
imports: [
MultiTenantModule.forRoot({
extractionStrategy: 'header',
tenantHeader: 'x-tenant-id',
}),
],
})
export class AppModule {}Fastify Cookies
For cookie-based tenant extraction with Fastify, use @fastify/cookie:
pnpm add @fastify/cookieimport fastifyCookie from '@fastify/cookie';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
await app.register(fastifyCookie);
await app.listen(3000);
}TenantContextService
Access tenant information from anywhere in your application using AsyncLocalStorage.
import { Injectable } from '@nestjs/common';
import { TenantContextService } from '@lexmata/nestjs-multi-tenant';
@Injectable()
export class AnyService {
constructor(private readonly tenantContext: TenantContextService) {}
doSomething() {
// Get full tenant object
const tenant = this.tenantContext.getTenant();
// Get just the ID
const tenantId = this.tenantContext.getTenantId();
// Check if in tenant context
if (this.tenantContext.hasTenant()) {
// In tenant context
}
}
}Running code in a tenant context programmatically
const tenant = { id: 'tenant-123', name: 'Test' };
tenantContext.run(tenant, () => {
// All code here has access to the tenant context
const id = tenantContext.getTenantId(); // 'tenant-123'
});Route Exclusions
Exclude specific routes from tenant extraction:
MultiTenantModule.forRoot({
extractionStrategy: 'header',
requireTenant: true,
excludeRoutes: [
'/health', // Exact match
'/api/public', // Prefix match
/^\/docs/, // Regex match
/^\/api\/v\d+\/public/, // Complex regex
],
})API Reference
MultiTenantModuleOptions
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| extractionStrategy | 'header' \| 'subdomain' \| 'path' \| 'query' \| 'cookie' \| 'jwt' \| 'bearer' \| 'custom' | 'header' | Strategy for extracting tenant ID |
| tenantHeader | string | 'x-tenant-id' | Header name for header strategy |
| tenantQueryParam | string | 'tenantId' | Query param for query strategy |
| tenantPathIndex | number | 0 | Path segment index for path strategy |
| tenantCookie | string | 'tenant_id' | Cookie name for cookie strategy |
| jwtTenantClaim | string | 'tenantId' | JWT claim path for jwt strategy (supports dot notation) |
| bearerTokenResolver | (token: string) => string \| null \| Promise<string \| null> | undefined | Function to resolve tenant ID from bearer token |
| tenantResolverCache | TenantCacheOptions | undefined | Cache configuration for tenant resolver results |
| tenantResolverCache.enabled | boolean | false | Enable caching of resolved tenants |
| tenantResolverCache.ttl | number | 300000 | Cache TTL in milliseconds (5 min default) |
| tenantResolverCache.max | number | 1000 | Maximum number of cached entries |
| eventHooks | TenantEventHooks | undefined | Lifecycle event hooks |
| eventHooks.onTenantIdExtracted | (id, ctx) => void | undefined | Called when tenant ID is extracted |
| eventHooks.onTenantResolved | (tenant, ctx) => void | undefined | Called when tenant is resolved |
| eventHooks.onTenantNotFound | (id, ctx) => void | undefined | Called when tenant resolver returns null |
| eventHooks.onTenantMissing | (ctx) => void | undefined | Called when no tenant ID in request |
| eventHooks.onTenantValidationFailed | (tenant, reason, ctx) => void | undefined | Called when tenant validation fails |
| tenantValidator | (tenant, ctx) => boolean \| TenantValidationResult | undefined | Validate tenant before allowing request |
| debug | boolean | false | Enable debug logging for tenant extraction |
| customExtractor | (req: Request) => string \| null \| Promise<string \| null> | - | Custom extraction function |
| tenantResolver | (id: string) => Tenant \| null \| Promise<Tenant \| null> | - | Resolve full tenant from ID |
| requireTenant | boolean | false | Throw if tenant not found |
| excludeRoutes | (string \| RegExp)[] | [] | Routes to skip |
Tenant Interface
interface Tenant {
id: string;
name?: string;
[key: string]: unknown;
}TenantContextService Methods
| Method | Return Type | Description |
|--------|-------------|-------------|
| getTenant() | Tenant \| undefined | Get current tenant |
| getTenantId() | string \| undefined | Get current tenant ID |
| hasTenant() | boolean | Check if in tenant context |
| run(tenant, fn) | T | Execute function in tenant context |
Testing
The module is fully tested with Vitest. Run tests with:
pnpm test # Run once
pnpm test:watch # Watch mode
pnpm test:coverage # With coverageExamples
Multi-tenant Database Connection
import { Injectable, Scope } from '@nestjs/common';
import { TenantContextService } from '@lexmata/nestjs-multi-tenant';
@Injectable({ scope: Scope.REQUEST })
export class TenantDatabaseService {
constructor(private readonly tenantContext: TenantContextService) {}
getConnection() {
const tenantId = this.tenantContext.getTenantId();
// Return tenant-specific database connection
return this.connectionPool.get(tenantId);
}
}Tenant-aware Repository
import { Injectable } from '@nestjs/common';
import { TenantContextService } from '@lexmata/nestjs-multi-tenant';
@Injectable()
export class UsersRepository {
constructor(
private readonly tenantContext: TenantContextService,
private readonly prisma: PrismaService,
) {}
findAll() {
const tenantId = this.tenantContext.getTenantId();
return this.prisma.user.findMany({
where: { tenantId },
});
}
}JWT-based Tenant Extraction
import { JwtService } from '@nestjs/jwt';
MultiTenantModule.forRootAsync({
imports: [JwtModule],
useFactory: (jwt: JwtService) => ({
extractionStrategy: 'custom',
customExtractor: (request) => {
const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) return null;
try {
const payload = jwt.verify(token);
return payload.tenantId;
} catch {
return null;
}
},
}),
inject: [JwtService],
})License
MIT © Lexmata LLC
