MongoDB Packages

@node-tenant ships two separate MongoDB packages — one for the raw native driver and one for the Mongoose ODM. This page covers both in full detail: architecture, API surface, multi-tenancy strategies, and complete real-world examples.

@node-tenant/tenant-mongodb@node-tenant/tenant-mongoose

Overview

What are these packages? If you want every single one of your customers to have their own completely separate MongoDB database (this is called database-per-tenant isolation), you need a way to easily switch databases. @node-tenant provides two separate packages to help you do this, depending on how you like to write your code:

Warning: These packages only handle the "Data Plane" (creating databases and switching between them). You still need @node-tenant/tenant-core to manage the master list of customers.

Low-level

tenant-mongodb

What it is: A wrapper around the official, raw mongodb driver.
Why use it: Gives you total, unrestricted access to the database without any extra overhead.
When to use it: When you need to run complex, custom MongoDB aggregations, or if you don't like using schemas.

High-level ODM

tenant-mongoose

What it is: A multi-tenant extension for Mongoose.
Why use it: Gives you all the magic of Mongoose (schemas, validation, hooks, .populate()) automatically scoped to the right customer's database.
When to use it: If your app is already built using Mongoose models.

Note: These packages are not alternatives for the same use case.tenant-mongodb is a "Database Adapter" that manages the tenant lifecycle (actually creating or deleting the MongoDB databases when a customer signs up). tenant-mongoose is just a helper that manages per-request query routing. You will usually use both together!

Side-by-Side Comparison

Feature tenant-mongodb tenant-mongoose
Underlying librarymongodbmongoose
Abstraction levelLow — raw driverHigh — ODM
Schemas & validation
pre / post hooks
populate() / virtuals
Typed modelsManual Generic Model<T>
ReturnsDbConnection / Model<T>
Tenant lifecycle (create/delete)
Connection pool cacheManual Built-in
Multi-tenant strategyDB per tenantDB per tenant (via connection)
Context-aware routingManual Auto via TenantContextManager
Best forCustom aggregations, raw queriesSchema-driven Mongoose apps

Package

tenant-mongodb

@node-tenant/tenant-mongodb is a DatabaseAdapter implementation that sits at the tenant lifecycle layer. It uses the official mongodb npm driver directly — no Mongoose, no ODM. Each tenant gets an isolated MongoDB database named {prefix}{slug} (default: tenant_acme).

Installation

npm install @node-tenant/tenant-core
npm install @node-tenant/tenant-mongodb
npm install mongodb  # peer dependency

How It Works

How does it actually work? Whenever a new customer signs up, this package tells your MongoDB server: "Hey, create a brand new database for this guy!". When that customer later visits your website, the @node-tenant/tenant-express middleware figures out who they are. Then, tenant-mongodb steps in and gives you a direct connection strictly to that customer's database.

Real-world Analogy

Imagine you rent out storage units. tenant-mongodb is the manager who builds a brand new storage unit every time someone rents one (Provisioning). When a renter shows up (HTTP Request), the manager hands them the exact key (the Db object) that ONLY opens their specific unit, making it physically impossible for them to accidentally open someone else's unit!

// Step 1: User visits "acme.myapp.com"
//       │
//       ▼
// Step 2: TenantMiddleware sees "acme", checks the Master List
//       │
//       ▼
// Step 3: TenantContextManager securely locks the request to "acme"
//       │
//       ├─► dbAdapter.switchTenant(activeTenant)
//       │        └─► Returns: client.db("tenant_acme")
//       │
//       └─► Step 4: Your route handler uses that specific database!

DB isolation

One MongoDB database per tenant — zero risk of data leaking across tenants.

Zero overhead

No ODM layer. Direct driver access means maximum query flexibility.

Lifecycle hooks

Full create, delete, migrate, and seed hooks for tenant provisioning workflows.

API Reference

createMongoDBDatabaseAdapter(options)DatabaseAdapter

Factory function. Creates the adapter from a connected MongoClient.

options.clientMongoClientA connected MongoClient instance.
options.dbPrefixstring?Prefix for database names. Default: 'tenant_'.
adapter.createTenantDatabase(tenant)Promise<void>

Pings the tenant database to lazily create it. MongoDB creates databases on first write.

tenantTenantTenant object from tenant-core.
adapter.deleteTenantDatabase(tenant)Promise<void>

Drops the entire tenant database. Irreversible.

tenantTenantTenant to delete.
adapter.switchTenant(tenant)Promise<Db>

Returns the MongoDB Db object for the given tenant.

tenantTenantTenant to switch to.
adapter.seedTenant(tenant, seedFn)Promise<void>

Runs a seeding function against the tenant's database.

tenantTenantTarget tenant.
seedFn(db: Db) => Promise<void>Your seed logic.

Code Examples

Basic Setup with Express

Here is a complete example of how to wire everything together in an Express app. We use the Prisma storage adapter for the Master List (Control Plane) and this MongoDB adapter for the actual user data.

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 to your MongoDB server
const mongoClient = new MongoClient(process.env.MONGODB_URI!);
await mongoClient.connect();

// 2. Setup the Data Plane (Where user data goes)
const dbAdapter = createMongoDBDatabaseAdapter({
  client: mongoClient,
  // If a customer's slug is "acme", their database will be named "tenant_acme"
  dbPrefix: 'tenant_',   
});

// 3. Setup the Control Plane (Where the master customer list is saved)
const prisma = new PrismaClient();
const storage = createPrismaStorage(prisma);

// 4. Combine them in the Manager
const manager = new TenantManager({ storage, databaseAdapter: dbAdapter });

const app = express();
app.use(express.json());

// 5. Secure all routes
app.use(tenantMiddleware({ manager, resolver: subdomainResolver() }));

// 6. Access the secure database in your routes!
app.get('/api/orders', async (req, res) => {
  // It securely fetches the "tenant_acme" database for us
  const db = await dbAdapter.switchTenant((req as any).tenant);
  
  // Notice we don't need a "WHERE tenantId = ..." filter! 
  // It's impossible to see another customer's orders because we are in a totally different database.
  const orders = await db.collection('orders').find({}).toArray();
  res.json(orders);
});

app.listen(3000);

Provisioning a New Tenant

// When a new customer signs up — provision their MongoDB database
async function provisionTenant(slug: string, name: string) {
  // 1. Create tenant record in the control-plane
  const tenant = await manager.createTenant({ slug, name });

  // 2. Create their isolated MongoDB database
  await dbAdapter.createTenantDatabase(tenant);

  // 3. Run seed data
  await dbAdapter.seedTenant(tenant, async (db) => {
    await db.collection('settings').insertOne({
      tenantId: tenant.id,
      plan: 'free',
      createdAt: new Date(),
    });
    await db.collection('users').createIndex({ email: 1 }, { unique: true });
  });

  console.log('Tenant provisioned:', slug);
}

await provisionTenant('acme', 'Acme Corp');

Deleting a Tenant

// Permanently remove a tenant and ALL their data
async function offboardTenant(slug: string) {
  const tenant = await manager.getTenantBySlug(slug);

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

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

  console.log('Tenant removed:', slug);
}
Warning: deleteTenantDatabase calls MongoDB's dropDatabase() — this is irreversible. Always take a backup before offboarding a tenant.

Package

tenant-mongoose

@node-tenant/tenant-mongoose is a query-routing layer for Mongoose. Rather than managing lifecycle operations, it reads the current tenant context from TenantContextManager and routes every Mongoose model operation to the correct tenant database — automatically.

Installation

npm install @node-tenant/tenant-core
npm install @node-tenant/tenant-mongoose
npm install mongoose  # peer dependency

How It Works

How does it actually work? The beauty of Mongoose is that you define your schemas (like User or Product) once, and you can re-use them across multiple databases. When a customer visits your app, this package takes your pre-defined Mongoose models and binds them exclusively to that customer's database on the fly.

Real-world Analogy

Imagine you have a cookie cutter (your Mongoose Schema). You have 10 different bowls of dough belonging to 10 different customers (the databases). tenant-mongoose simply grabs the correct bowl of dough for the current customer before pressing the cookie cutter down!

// Step 1: User visits "acme.myapp.com"
//       │
//       ▼
// Step 2: TenantMiddleware sees "acme", sets the Global Context
//       │
//       ▼
// Step 3: You call useTenantModel('User', UserSchema) in your route
//       │
//       ├─► It asks the Context for the active customer ("acme")
//       │
//       ├─► It asks the Connection Pool for the "acme" connection
//       │
//       └─► Step 4: It returns a standard Mongoose Model permanently locked to "tenant_acme"!

Schema + validation

Full Mongoose schema power — types, validators, virtuals, and middleware hooks.

Connection pooling

Connections are cached per tenant slug — no overhead on repeated requests.

Context-aware

Automatically reads the current tenant from TenantContextManager — no manual wiring.

API Reference

createConnectionPool(baseUri, dbPrefix?)ConnectionFactory

Creates a memoized connection factory. Pass the returned function to useTenantConnection or useTenantModel.

baseUristringMongoDB connection string without database name. e.g. mongodb://localhost:27017
dbPrefixstring?Database name prefix. Default: 'tenant_'.
useTenantConnection(factory)Connection

Reads the current tenant context and returns the Mongoose Connection for that tenant. Throws if called outside a tenant context.

factoryConnectionFactoryThe factory returned by createConnectionPool.
useTenantModel<T>(name, schema, factory)Model<T>

Returns a tenant-scoped Mongoose Model. Re-uses a registered model if it already exists on the connection.

namestringModel name e.g. 'User'.
schemaSchema<T>Mongoose schema definition.
factoryConnectionFactoryThe factory returned by createConnectionPool.

Code Examples

Define Schemas (shared, tenant-agnostic)

// schemas/User.ts
import { Schema } from 'mongoose';

// 1. Define the shape of your data
export interface IUser {
  name: string;
  email: string;
  role: 'admin' | 'member';
  createdAt: Date;
}

// 2. Create the raw Mongoose Schema
// Notice we DO NOT call mongoose.model() here!
export const UserSchema = new Schema<IUser>({
  name:      { type: String, required: true },
  email:     { type: String, required: true, unique: true },
  role:      { type: String, enum: ['admin', 'member'], default: 'member' },
  createdAt: { type: Date, default: Date.now },
});

// All your normal Mongoose hooks, virtuals, and methods work perfectly
UserSchema.pre('save', function (next) {
  this.email = this.email.toLowerCase();
  next();
});

Setup & Route Handler with Express

Here is a full example of using the Models securely inside your API routes.

import express from 'express';
import { TenantManager } from '@node-tenant/tenant-core';
import { tenantMiddleware, subdomainResolver } from '@node-tenant/tenant-express';
import { createConnectionPool, useTenantModel } from '@node-tenant/tenant-mongoose';
import { UserSchema, IUser } from './schemas/User';

// 1. Create a Connection Pool
// This is like a factory that produces database connections when needed.
const getConnection = createConnectionPool(
  process.env.MONGODB_URI!,  
  'tenant_' // e.g. tenant_acme
);

const app = express();
app.use(express.json());

// 2. Secure your app with the middleware
app.use(tenantMiddleware({ manager, resolver: subdomainResolver() }));

// 3. Write your routes
app.get('/api/users', async (req, res) => {
  // Magic! This automatically gives you the 'User' model for the active customer
  const User = useTenantModel<IUser>('User', UserSchema, getConnection);
  
  // It only fetches users from their database
  const users = await User.find({ role: 'member' });
  res.json(users);
});

app.post('/api/users', async (req, res) => {
  const User = useTenantModel<IUser>('User', UserSchema, getConnection);
  const user = new User(req.body);
  
  // Mongoose pre-save hooks will run normally!
  await user.save(); 
  res.status(201).json(user);
});

app.listen(3000);

Using useTenantConnection directly

If you need to use multiple models at once, it's faster to grab the connection directly and bind your models to it.

import { useTenantConnection } from '@node-tenant/tenant-mongoose';

// Grab the raw Mongoose Connection for the active customer
const conn = useTenantConnection(getConnection);

// Bind multiple schemas at once
const User    = conn.model<IUser>('User', UserSchema);
const Product = conn.model<IProduct>('Product', ProductSchema);
const Order   = conn.model<IOrder>('Order', OrderSchema);

// Now use them normally!
const users    = await User.find();
const products = await Product.find({ inStock: true });
Tip: Define your schemas once (outside of any request handler) and reuse them across tenants. The schema objects themselves are stateless — only the Connection carries tenant identity.

Multi-Tenant Strategies

Understanding when to use which package is critical for your data isolation strategy:

  • Strategy 1: Database-per-Tenant (Dedicated Isolation). If you need maximum isolation, use the @node-tenant/tenant-mongodb or @node-tenant/tenant-mongoose packages described on this page. They dynamically provision and switch databases per tenant.
  • Strategy 2: Shared Database (`tenantId`) (Logical Isolation). If you want all tenants to share the same database, you do not need the adapters on this page. Instead, use the raw MongoDB driver alongside the @node-tenant/tenant-storage-mongodb package, manually filtering queries by a tenantId field.

Database per Tenant ✓ (tenant-mongodb packages)

Isolation: MaximumPerformance: HighComplexity: Medium

Best for: Enterprise / regulated industries (HIPAA, GDPR)

Shared Collection (tenantId field)

Isolation: LowPerformance: HighestComplexity: Lowest

Best for: High-scale apps where isolation is less critical

Strategy 1: Database-per-Tenant (Using Adapters)

Use createMongoDBDatabaseAdapter to automatically provision and switch to separate databases. This guarantees your customers can never see each other's data, even if you make a mistake in your code!

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

const dbAdapter = createMongoDBDatabaseAdapter({ client: mongoClient });
const manager = new TenantManager({ storage, databaseAdapter: dbAdapter });

app.get('/api/orders', async (req, res) => {
  // It securely switches to tenant_acme, tenant_globex, etc. for you
  const db = await dbAdapter.switchTenant((req as any).tenant);
  const orders = await db.collection('orders').find({}).toArray();
  res.json(orders);
});

Strategy 2: Shared Database (tenantId)

Omit the database adapter entirely. You just use one giant database and you are responsible for always adding { tenantId: ... } to your queries.

// No database adapter provided here!
const manager = new TenantManager({ storage }); 

app.get('/api/orders', async (req, res) => {
  const activeCustomer = (req as any).tenant;
  
  // DANGER: You MUST remember to include tenantId here, otherwise 
  // you will return everyone's orders to this customer!
  const orders = await masterDb.collection('orders').find({ tenantId: activeCustomer.id }).toArray();
  res.json(orders);
});

app.post('/api/orders', async (req, res) => {
  const activeCustomer = (req as any).tenant;
  // Always attach tenantId when saving new data
  await masterDb.collection('orders').insertOne({ ...req.body, tenantId: activeCustomer.id });
  res.status(201).send();
});

Which One Should I Use?

Use tenant-mongodb if…

  • You are not using Mongoose — raw driver only
  • You need full tenant lifecycle management (create, delete, migrate)
  • You write complex aggregation pipelines
  • You need fine-grained control over indexes and collections
  • You're integrating with a non-Mongoose migration tool

Use tenant-mongoose if…

  • Your app already uses Mongoose
  • You want Schema validation and typed models
  • You rely on pre/post hooks, virtuals, or populate()
  • You want zero boilerplate for per-request tenant routing
  • You're building a standard CRUD API without complex pipelines

Using Both Together

The most production-ready setup uses both packages together: tenant-mongodb handles the tenant lifecycle (provisioning, deletion, seeding) while tenant-mongoose handles per-request query routing through Mongoose models.

import { MongoClient } from 'mongodb';
import { TenantManager } from '@node-tenant/tenant-core';
import { createMongoDBDatabaseAdapter } from '@node-tenant/tenant-mongodb';
import {
  createConnectionPool,
  useTenantModel,
} from '@node-tenant/tenant-mongoose';

const mongoClient = new MongoClient(process.env.MONGODB_URI!);
await mongoClient.connect();

// tenant-mongodb handles lifecycle
const dbAdapter = createMongoDBDatabaseAdapter({ client: mongoClient });

// tenant-mongoose handles per-request query routing
const getConnection = createConnectionPool(process.env.MONGODB_URI!);

// --- TENANT PROVISIONING (use tenant-mongodb) ---
async function onNewCustomer(slug: string) {
  const tenant = await manager.createTenant({ slug });
  await dbAdapter.createTenantDatabase(tenant);  //  tenant-mongodb
  await dbAdapter.seedTenant(tenant, async (db) => {
    await db.collection('settings').insertOne({ plan: 'free' });
  });
}

// --- PER-REQUEST QUERY (use tenant-mongoose) ---
app.get('/api/products', async (req, res) => {
  const Product = useTenantModel('Product', ProductSchema, getConnection); //  tenant-mongoose
  const products = await Product.find({ active: true });
  res.json(products);
});
Tip: This is the recommended architecture for production Mongoose apps. Use tenant-mongodb in your admin / DevOps scripts and use tenant-mongoose inside your request handlers and service layer.
Back to Docs
Packages:@node-tenant/tenant-mongodb&@node-tenant/tenant-mongoose