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.
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:
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.
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 library | mongodb | mongoose |
| Abstraction level | Low — raw driver | High — ODM |
| Schemas & validation | ✕ | ✓ |
| pre / post hooks | ✕ | ✓ |
| populate() / virtuals | ✕ | ✓ |
| Typed models | Manual | ✓ Generic Model<T> |
| Returns | Db | Connection / Model<T> |
| Tenant lifecycle (create/delete) | ✓ | ✕ |
| Connection pool cache | Manual | ✓ Built-in |
| Multi-tenant strategy | DB per tenant | DB per tenant (via connection) |
| Context-aware routing | Manual | ✓ Auto via TenantContextManager |
| Best for | Custom aggregations, raw queries | Schema-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)→ DatabaseAdapterFactory 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);
}
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?)→ ConnectionFactoryCreates a memoized connection factory. Pass the returned function to useTenantConnection or useTenantModel.
baseUristringMongoDB connection string without database name. e.g. mongodb://localhost:27017dbPrefixstring?Database name prefix. Default: 'tenant_'.useTenantConnection(factory)→ ConnectionReads 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 });
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-mongodbor@node-tenant/tenant-mongoosepackages 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
tenantIdfield.
Database per Tenant ✓ (tenant-mongodb packages)
Best for: Enterprise / regulated industries (HIPAA, GDPR)
Shared Collection (tenantId field)
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);
});
tenant-mongodb in your admin / DevOps scripts and use tenant-mongoose inside your request handlers and service layer.@node-tenant/tenant-mongodb&@node-tenant/tenant-mongoose