DEV Community

Cover image for Multi-tenant Mongoose Module for NestJS
phen0menon
phen0menon

Posted on

Multi-tenant Mongoose Module for NestJS

Building a multi-tenant service? If you're using NestJS and MongoDB (Mongoose), you've probably hit a wall when trying to isolate customer data.

The common approach - database-per-tenant - often leads to messy code where you have to manually pass tenant IDs around or dynamically create connections on every single request. It gets even worse when you add background jobs into the mix.

I got tired of the boilerplate and edge cases, so I built @phen0menon/nestjs-mongoose-tenancy.

The main idea

The goal was to make it feel exactly like the official @nestjs/mongoose package, but tenant-aware.

Instead of doing manual lookups in your business logic, the library handles it at the Dependency Injection layer. You just define a resolver in your app.module.ts (like grabbing an x-tenant-id header), and the DI container gives you the correct model.

@Injectable()
export class ProductService {
  constructor(
    // It just gives you the model connected to the right DB πŸ‘‡
    @InjectTenantModel(Product.name)
    private readonly products: Model<ProductDocument>,
  ) {}

  async getProducts() {
    return this.products.find().lean();
  }
}
Enter fullscreen mode Exit fullscreen mode

If a request comes in without a tenant ID, or with an unknown one, it gracefully falls back to a mandatory common database (which is also great for storing global app settings or user accounts).

Background jobs?

This was my biggest headache with other approaches. Workers (like BullMQ or cron jobs) don't have an HTTP context, so request-scoped injection breaks.

To fix this, the library exposes an @InjectTenantModelMap() decorator. It gives you a ReadonlyMap of all your initialized tenant models. When your worker picks up a job, you just grab the specific model you need:

@Injectable()
export class ProductJobHandler {
  constructor(
    @InjectTenantModelMap(Product.name)
    private readonly productModels: ReadonlyMap<string, Model<ProductDocument>>,
  ) {}

  async processJob(jobData: { tenantId: string }) {
    const model = this.productModels.get(jobData.tenantId);
    if (!model) throw new Error('Tenant disconnected or missing');

    return model.countDocuments();
  }
}
Enter fullscreen mode Exit fullscreen mode

Dealing with Startup Failures

In a multi-tenant app, if one customer's database goes down, you really don't want your entire NestJS app to crash on startup.

By default, the library is fail-fast. But I added a degraded startup mode. If a tenant DB is unreachable when the app boots, it skips it and starts the app anyway. Any HTTP requests routed to that specific tenant will throw a clean TenantConnectionUnavailableException until the connection recovers.

A few other things it does

  • Dynamic discovery: You don't have to hardcode tenant URIs. You can load your tenants from your common DB asynchronously during the app bootstrap phase.
  • Zero runtime overhead: Because models are resolved at the DI level, tenant switching is blazing fast.
  • Strict typing: It's written in TS and plays nice with strict mode.

I've tested it with NestJS 9-11 and Mongoose 6-9.

If you're building something similar and want to skip writing all this plumbing yourself, you can check it out here:

πŸ”— GitHub: phen0menon/nestjs-mongoose-tenancy
πŸ“¦ NPM: @phen0menon/nestjs-mongoose-tenancy

There are a few runnable examples in the repo (including one using mongodb-memory-server so you can actually test the connections locally). Let me know if you run into any issues or have ideas for improvements ✌️

Top comments (0)