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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@node-tenant/tenant-mongodb

v0.1.3

Published

MongoDB database adapter for @node-tenant/tenant (database-per-tenant strategy)

Readme

@node-tenant/tenant-mongodb

Database-per-tenant adapter for the @node-tenant ecosystem using the official MongoDB native driver.

🌐 Website: node-tenant.com

npm version license


Overview

@node-tenant/tenant-mongodb implements the DatabaseAdapter interface from @node-tenant/tenant-core. It gives each tenant a fully isolated MongoDB database (tenant_acme, tenant_globex, ...) using the official mongodb npm driver — no Mongoose, no ODM overhead.

Use this package when you need:

  • Full tenant lifecycle management (create, delete, migrate, seed databases)
  • Direct access to the raw MongoDB driver (Db, Collection, aggregation pipelines)
  • Maximum query flexibility without schema constraints

Table of Contents


Installation

npm install @node-tenant/tenant-core @node-tenant/tenant-mongodb mongodb
pnpm add @node-tenant/tenant-core @node-tenant/tenant-mongodb mongodb
yarn add @node-tenant/tenant-core @node-tenant/tenant-mongodb mongodb

Peer dependency: mongodb >= 6.0.0 must be installed in your project.


Architecture

HTTP Request (acme.myapp.com)
       │
       ▼
TenantMiddleware ──► resolves slug "acme"
       │
       ▼
TenantContextManager.run(tenant, callback)
       │
       ├──► adapter.switchTenant(tenant)
       │         └──► client.db("tenant_acme")   ← isolated Db
       │
       └──► Your route handler
                 └──► db.collection('orders').find({})
                            ↑ only queries tenant_acme database

Each tenant has their own MongoDB database. Data is completely isolated at the database level — no WHERE tenantId = filtering needed.


Quick Start

import { MongoClient } from 'mongodb';
import express from 'express';
import { TenantManager } from '@node-tenant/tenant-core';
import { tenantMiddleware, subdomainResolver } from '@node-tenant/tenant-express';
import { createMongoDBDatabaseAdapter } from '@node-tenant/tenant-mongodb';
import { createPrismaStorage } from '@node-tenant/tenant-storage-prisma';
import { PrismaClient } from '@prisma/client';

// 1. Connect MongoDB
const mongoClient = new MongoClient(process.env.MONGODB_URI!);
await mongoClient.connect();

// 2. Create the adapter — each tenant gets a database named "tenant_<slug>"
const dbAdapter = createMongoDBDatabaseAdapter({
  client: mongoClient,
  dbPrefix: 'tenant_', // default
});

// 3. Control-plane storage (stores tenant metadata)
const prisma = new PrismaClient();
const storage = createPrismaStorage(prisma);

// 4. Tenant manager wiring
const manager = new TenantManager({ storage, databaseAdapter: dbAdapter });

// 5. Express app
const app = express();
app.use(express.json());
app.use(tenantMiddleware({ manager, resolver: subdomainResolver(storage) }));

// 6. Route — use the raw MongoDB Db per request
app.get('/api/orders', async (req, res) => {
  const db = await dbAdapter.switchTenant((req as any).tenant);
  const orders = await db.collection('orders').find({}).toArray();
  res.json(orders);
});

app.listen(3000, () => console.log('Listening on port 3000'));

API Reference

createMongoDBDatabaseAdapter(options)

Factory function that creates a DatabaseAdapter.

import { createMongoDBDatabaseAdapter } from '@node-tenant/tenant-mongodb';

const adapter = createMongoDBDatabaseAdapter({
  client: mongoClient,   // MongoClient — required
  dbPrefix: 'tenant_',   // string — optional, default: 'tenant_'
});

| Option | Type | Required | Default | Description | |---|---|---|---|---| | client | MongoClient | ✅ | — | A connected MongoClient instance | | dbPrefix | string | ❌ | 'tenant_' | Prefix for all tenant database names |


adapter.createTenantDatabase(tenant)

Creates (or verifies) a tenant's MongoDB database. MongoDB creates databases lazily on first write, so this method pings the database to initialize it.

await adapter.createTenantDatabase(tenant);
// Logs: [tenant-mongodb] Database ready: tenant_acme

| Parameter | Type | Description | |---|---|---| | tenant | Tenant | The tenant object from tenant-core |

Returns: Promise<void>


adapter.deleteTenantDatabase(tenant)

Permanently drops the tenant's entire MongoDB database. This is irreversible.

await adapter.deleteTenantDatabase(tenant);

⚠️ Warning: This calls MongoDB's dropDatabase(). Always take a backup before calling this in production.

Returns: Promise<void>


adapter.switchTenant(tenant)

Returns the Db object for the given tenant. Use this inside request handlers to get the tenant-scoped database.

const db = await adapter.switchTenant(tenant);
const users = await db.collection('users').find({}).toArray();

| Parameter | Type | Description | |---|---|---| | tenant | Tenant | The active tenant |

Returns: Promise<Db>


adapter.migrateTenant(tenant, migrationPath?)

Hook for running migrations on a specific tenant's database. By default this is a no-op — provide your own implementation using a migration tool (e.g. migrate-mongo).

await adapter.migrateTenant(tenant, './migrations');

Returns: Promise<void>


adapter.migrateAll()

Hook for migrating all tenant databases. No-op by default.

await adapter.migrateAll();

Returns: Promise<void>


adapter.seedTenant(tenant, seedFn)

Runs a seed function against the tenant's database. The seedFn receives the raw Db object.

await adapter.seedTenant(tenant, async (db) => {
  await db.collection('settings').insertOne({ plan: 'free' });
  await db.collection('users').createIndex({ email: 1 }, { unique: true });
});

| Parameter | Type | Description | |---|---|---| | tenant | Tenant | Target tenant | | seedFn | (db: Db) => Promise<void> | Your seed logic. Required. |

Returns: Promise<void>


Examples

1. Basic Express Setup

import { MongoClient } from 'mongodb';
import express from 'express';
import { createMongoDBDatabaseAdapter } from '@node-tenant/tenant-mongodb';

const client = new MongoClient('mongodb://localhost:27017');
await client.connect();

const adapter = createMongoDBDatabaseAdapter({ client });
const app = express();

// Assuming tenantMiddleware is already resolving (req as any).tenant
app.get('/api/products', async (req, res) => {
  const db = await adapter.switchTenant((req as any).tenant);
  const products = await db.collection('products').find({ inStock: true }).toArray();
  res.json(products);
});

app.post('/api/products', async (req, res) => {
  const db = await adapter.switchTenant((req as any).tenant);
  const result = await db.collection('products').insertOne({
    ...req.body,
    createdAt: new Date(),
  });
  res.status(201).json({ id: result.insertedId });
});

2. Provision a New Tenant

Called when a new customer signs up:

import { createMongoDBDatabaseAdapter } from '@node-tenant/tenant-mongodb';

async function provisionTenant(slug: string, name: string) {
  // Step 1: Register tenant in control-plane
  const tenant = await manager.createTenant({
    slug,
    name,
    status: 'active',
    strategy: 'database',
  });

  // Step 2: Create their isolated MongoDB database
  await adapter.createTenantDatabase(tenant);

  // Step 3: Seed initial data
  await adapter.seedTenant(tenant, async (db) => {
    await db.collection('settings').insertOne({
      plan: 'starter',
      maxUsers: 5,
      createdAt: new Date(),
    });

    // Create indexes
    await db.collection('users').createIndex({ email: 1 }, { unique: true });
    await db.collection('orders').createIndex({ createdAt: -1 });
  });

  console.log(`✅ Tenant "${slug}" provisioned successfully`);
  return tenant;
}

// Usage
await provisionTenant('acme', 'Acme Corp');
await provisionTenant('globex', 'Globex Inc');

3. Seed Tenant Database

async function seedTenant(tenantSlug: string) {
  const tenant = await manager.getTenantBySlug(tenantSlug);

  await adapter.seedTenant(tenant, async (db) => {
    // Insert sample data
    await db.collection('categories').insertMany([
      { name: 'Electronics', slug: 'electronics' },
      { name: 'Clothing', slug: 'clothing' },
      { name: 'Books', slug: 'books' },
    ]);

    // Create compound index
    await db.collection('products').createIndex(
      { category: 1, price: -1 },
      { name: 'category_price_idx' }
    );

    console.log(`Seeded tenant: ${tenantSlug}`);
  });
}

4. Delete a Tenant

Permanently removes a tenant and all their data:

async function offboardTenant(tenantId: string) {
  const tenant = await manager.getTenantById(tenantId);

  console.log(`Offboarding tenant: ${tenant.slug}`);

  // Drop the entire MongoDB database
  await adapter.deleteTenantDatabase(tenant);

  // Remove from control-plane
  await manager.deleteTenant(tenantId);

  console.log(`✅ Tenant "${tenant.slug}" removed`);
}

5. Custom Database Prefix

// Databases will be named: app_acme, app_globex, app_startup
const adapter = createMongoDBDatabaseAdapter({
  client: mongoClient,
  dbPrefix: 'app_',
});

6. Aggregation Pipeline

The raw Db object gives you full access to MongoDB aggregation:

app.get('/api/analytics/revenue', async (req, res) => {
  const db = await adapter.switchTenant((req as any).tenant);

  const pipeline = [
    { $match: { status: 'paid', createdAt: { $gte: new Date('2026-01-01') } } },
    { $group: { _id: { $month: '$createdAt' }, total: { $sum: '$amount' } } },
    { $sort: { '_id': 1 } },
  ];

  const revenue = await db.collection('invoices').aggregate(pipeline).toArray();
  res.json(revenue);
});

7. NestJS Integration

// mongodb.module.ts
import { Module, Global } from '@nestjs/common';
import { MongoClient } from 'mongodb';
import { createMongoDBDatabaseAdapter } from '@node-tenant/tenant-mongodb';

@Global()
@Module({
  providers: [
    {
      provide: 'MONGO_CLIENT',
      useFactory: async () => {
        const client = new MongoClient(process.env.MONGODB_URI!);
        await client.connect();
        return client;
      },
    },
    {
      provide: 'MONGO_ADAPTER',
      useFactory: (client: MongoClient) =>
        createMongoDBDatabaseAdapter({ client, dbPrefix: 'tenant_' }),
      inject: ['MONGO_CLIENT'],
    },
  ],
  exports: ['MONGO_CLIENT', 'MONGO_ADAPTER'],
})
export class MongoDBModule {}
// orders.service.ts
import { Injectable, Inject } from '@nestjs/common';
import type { DatabaseAdapter } from '@node-tenant/tenant-core';
import { TenantContextManager } from '@node-tenant/tenant-core';
import type { Db } from 'mongodb';

@Injectable()
export class OrdersService {
  constructor(
    @Inject('MONGO_ADAPTER') private readonly adapter: DatabaseAdapter,
  ) {}

  private async getDb(): Promise<Db> {
    const tenant = TenantContextManager.currentOrThrow();
    return this.adapter.switchTenant(tenant) as Promise<Db>;
  }

  async findAll() {
    const db = await this.getDb();
    return db.collection('orders').find({}).toArray();
  }

  async create(data: Record<string, unknown>) {
    const db = await this.getDb();
    return db.collection('orders').insertOne({ ...data, createdAt: new Date() });
  }
}

Multi-Tenant Strategies

This package explicitly implements the Database-per-Tenant strategy (the database strategy). The createMongoDBDatabaseAdapter always connects to a separate database per tenant, isolated by the tenant's slug.

| Strategy | Isolation | Best For | Support in this package | |---|---|---|---| | Database per Tenant | 🔒 Maximum | Enterprise, GDPR/HIPAA regulated apps | ✅ Full (via createMongoDBDatabaseAdapter) | | Shared Collection (tenantId) | 🔓 Minimal | High-scale apps where isolation is less critical | ❌ Requires manual implementation (see below) |

How to use the tenantId strategy (Shared Database)

If you configure a tenant with strategy: 'tenantId', you should not use createMongoDBDatabaseAdapter in your request handlers, as it forces database-level isolation. Instead, use a single shared database instance and manually append the tenantId to all your MongoDB queries:

import { MongoClient } from 'mongodb';
import { TenantManager } from '@node-tenant/tenant-core';

// 1. Connect to single shared database
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
const masterDb = client.db('master_db');

// 2. Initialize manager without a DatabaseAdapter
const manager = new TenantManager({ storage });

// 3. In your route handlers, manually scope queries by tenantId
app.get('/api/orders', async (req, res) => {
  const tenant = (req as any).tenant;
  
  // Notice we use the masterDb and append tenantId to the query filter
  const orders = await masterDb.collection('orders').find({ tenantId: tenant.id }).toArray();
  res.json(orders);
});

app.post('/api/orders', async (req, res) => {
  const tenant = (req as any).tenant;
  
  // Attach tenantId when inserting new documents
  const result = await masterDb.collection('orders').insertOne({
    ...req.body,
    tenantId: tenant.id
  });
  res.json({ id: result.insertedId });
});

Comparison: tenant-mongodb vs tenant-mongoose

| | tenant-mongodb | tenant-mongoose | |---|---|---| | Underlying library | mongodb (native driver) | mongoose (ODM) | | Returns | Raw Db object | Connection / Model<T> | | Schemas & validation | ❌ Manual | ✅ Full Mongoose schemas | | Tenant lifecycle | ✅ Full (create/delete/seed/migrate) | ❌ Not included | | Best for | Custom pipelines, raw queries | Schema-driven Mongoose apps |

Tip: Use tenant-mongodb for lifecycle management and tenant-mongoose for per-request query routing. They work great together.


FAQ

Q: Does each tenant get a separate MongoDB connection?
A: No. All tenants share the same MongoClient connection pool. switchTenant() just calls client.db("tenant_slug") which is lightweight and requires no new TCP connections.

Q: What happens if I call switchTenant() for a non-existent tenant?
A: MongoDB will create the database lazily on the first write. If you only read, the database won't be created. Always call createTenantDatabase() during provisioning.

Q: Can I use this with Atlas or other cloud MongoDB providers?
A: Yes. Pass your Atlas connection URI to MongoClient. All cloud MongoDB deployments are supported as long as mongodb >= 6.0.0 is compatible.

Q: How do I run migrations?
A: The migrateTenant hook is a no-op by default. You can integrate any migration tool (e.g. migrate-mongo) inside it.


License

MIT © Anupam Vishwakarma