@node-tenant/tenant-mongodb
v0.1.3
Published
MongoDB database adapter for @node-tenant/tenant (database-per-tenant strategy)
Maintainers
Readme
@node-tenant/tenant-mongodb
Database-per-tenant adapter for the
@node-tenantecosystem using the official MongoDB native driver.🌐 Website: node-tenant.com
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
- Architecture
- Quick Start
- API Reference
- Examples
- Multi-Tenant Strategies
- Comparison: tenant-mongodb vs tenant-mongoose
- FAQ
Installation
npm install @node-tenant/tenant-core @node-tenant/tenant-mongodb mongodbpnpm add @node-tenant/tenant-core @node-tenant/tenant-mongodb mongodbyarn add @node-tenant/tenant-core @node-tenant/tenant-mongodb mongodbPeer dependency:
mongodb >= 6.0.0must 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 databaseEach 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-mongodbfor lifecycle management andtenant-mongoosefor 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
