MongoDB Storage Adapter

A high-performance Control Plane storage layer powered entirely by MongoDB. Store and manage your tenants, domains, and organizations using a single native MongoClient instance.

@node-tenant/tenant-storage-mongodb

Overview

What is this package? In a multi-tenant platform, you need a "master database" that keeps track of who all your customers are, what their custom domain names are, and whether their accounts are active. This master database is called the Control Plane. This package is an adapter that teaches the core TenantManager how to save and read this master list using MongoDB.

Real-world Analogy

Imagine you are building a hotel management system. The TenantManager is the Receptionist. But the Receptionist needs a physical ledger to write down the names of the guests. This tenant-storage-mongodb package provides that ledger, formatted specifically for MongoDB!

Tip: If your entire backend is built on MongoDB, you can use this package alongside @node-tenant/tenant-mongodb to create a 100% NoSQL multi-tenant architecture!

Installation

Install this package alongside the core engine. You will also need the official mongodb driver installed in your project.

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

Initialization

How to use it: You will connect to MongoDB, select a specific database for your master list (e.g. master_tenants_db), pass that to createMongoStorage, and then give the result to the TenantManager.

import { MongoClient } from 'mongodb';
import { TenantManager } from '@node-tenant/tenant-core';
// Import the MongoDB storage adapter
import { createMongoStorage } from '@node-tenant/tenant-storage-mongodb';

// 1. Connect to your MongoDB server normally
const mongoClient = new MongoClient("mongodb://localhost:27017/");
await mongoClient.connect();

// 2. Choose which database will act as the "master ledger"
const masterDb = mongoClient.db('master_tenants_db');

// 3. Wrap the database in the Storage Adapter so the Manager can understand it
const storage = createMongoStorage(masterDb);

// 4. Initialize the Core Tenant Manager with this storage
const manager = new TenantManager({ storage });

// Example: Create a new customer workspace!
// Behind the scenes, this runs: masterDb.collection('tenants').insertOne(...)
await manager.createTenant({
  name: 'Acme Corp',
  slug: 'acme',
  status: 'active',
  strategy: 'database',
});

Collections Created

When you run the code above, the adapter will automatically create and manage several MongoDB Collections inside your master database. Here is what they are used for:

  • tenants: The main list of all your customers.
  • tenant_organizations: Used if you want to group multiple tenants under a single parent company.
  • tenant_domains: Used if you allow customers to map their own custom domains (e.g., app.acme.com).
  • tenant_relationships: Manages B2B connections between different tenants.
  • resource_shares: Handles permissions if one tenant shares a file with another tenant.
  • audit_logs: A permanent history of changes made to tenant settings.

Full Integration (Data Plane)

The Control Plane (this package) manages the master list of customers. But how do you secure the actual user data (the Data Plane)? You have two main choices when using MongoDB:

  • Strategy 1: Database-per-Tenant (Highest Security). Every single customer gets their own separate MongoDB Database. You will use this package alongside @node-tenant/tenant-mongodb to achieve this automatically.
  • Strategy 2: Shared Database (Easiest Setup). All customers share one giant MongoDB Database. Every document has a tenantId field. You don't need any other packages, you just filter queries manually using the active tenantId.

Strategy 1: Database-per-Tenant

In this strategy, we configure the TenantManager with TWO adapters: a Storage Adapter (for the master list) and a Database Adapter (to handle switching the active database for the user's data).

import express from 'express';
import { MongoClient } from 'mongodb';
import { TenantManager } from '@node-tenant/tenant-core';
import { tenantMiddleware, subdomainResolver } from '@node-tenant/tenant-express';
// 1. Import BOTH the Storage (Control) and Database (Data) adapters
import { createMongoStorage } from '@node-tenant/tenant-storage-mongodb';
import { createMongoDBDatabaseAdapter } from '@node-tenant/tenant-mongodb';

const mongoClient = new MongoClient("mongodb://localhost:27017/");
await mongoClient.connect();

// 2. Setup Control Plane: The Master List
const storage = createMongoStorage(mongoClient.db('master_db'));

// 3. Setup Data Plane: The automatic database switcher
const dbAdapter = createMongoDBDatabaseAdapter({
  client: mongoClient,
  // If a tenant's slug is "acme", their database will be named "tenant_acme"
  dbPrefix: 'tenant_', 
});

// 4. Initialize Manager with BOTH adapters
const manager = new TenantManager({ storage, databaseAdapter: dbAdapter });

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

// 5. In your routes, you can now safely ask the dbAdapter for the isolated database!
app.get('/api/orders', async (req, res) => {
  // It automatically returns the "tenant_acme" database based on the active request
  const db = await dbAdapter.switchTenant((req as any).tenant);
  const orders = await db.collection('orders').find({}).toArray();
  res.json(orders);
});

app.listen(3000);

Strategy 2: Shared Database (tenantId)

If you prefer to keep all tenant data in a single shared database, simply omit the database adapter entirely and manually filter by tenantId in your queries.

import express from 'express';
import { MongoClient } from 'mongodb';
import { TenantManager } from '@node-tenant/tenant-core';
import { tenantMiddleware, subdomainResolver } from '@node-tenant/tenant-express';
import { createMongoStorage } from '@node-tenant/tenant-storage-mongodb';

const mongoClient = new MongoClient("mongodb://localhost:27017/");
await mongoClient.connect();

const masterDb = mongoClient.db('master_db');
const storage = createMongoStorage(masterDb);

// Notice we only provide the storage adapter here!
const manager = new TenantManager({ storage });

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

// Route handlers connect to the giant master database and filter manually using the active tenant's ID
app.get('/api/orders', async (req, res) => {
  const activeTenant = (req as any).tenant;
  // Always remember to include the tenantId filter!
  const orders = await masterDb.collection('orders').find({ tenantId: activeTenant.id }).toArray();
  res.json(orders);
});

app.listen(3000);