npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@lexmata/nestjs-multi-tenant

v0.1.1

Published

A NestJS module for building multi-tenant applications

Readme

@lexmata/nestjs-multi-tenant

npm version npm downloads CI codecov License: MIT Node.js Version NestJS TypeScript

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-tenant

Peer 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/users

Subdomain 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-parser middleware. If cookie-parser is not used, cookies are parsed from the Cookie header 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 jwt strategy which decodes the token, bearer strategy 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 tenant

Use 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-123

When 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:

  1. client.tenant - Direct property on socket
  2. client.handshake.tenant - Set during handshake
  3. client.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:

  1. data.tenant - Full tenant object in message payload
  2. data.tenantId - Tenant ID in payload (wrapped as { id: tenantId })
  3. context.tenant - Set by interceptor or middleware
  4. context.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/cookie
import 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 coverage

Examples

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