tenant-mongoose Documentation
Complete, production-ready guide for the @node-tenant/tenant-mongoose package.
1. Introduction
What is `tenant-mongoose`?
It is a per-request Mongoose connection router for the @node-tenant ecosystem. It reads the active tenant context and automatically routes every Mongoose model operation to the correct tenant database.
What problem it solves:
In a multi-tenant SaaS, you often want a strict database-per-tenant architecture to ensure data isolation. However, manually creating and managing Mongoose connections for every request is tedious and error-prone. tenant-mongoose solves this by caching connections per tenant and providing scoped models on the fly.
How multi-tenancy works specifically in this project:
The architecture implemented in the codebase uses dynamic connection mapping. 1. A request enters the app, and a middleware determines the tenant (e.g., via subdomain). 2. The middleware sets the tenant slug in the TenantContextManager (from tenant-core). 3. When you use useTenantModel, it retrieves the current slug, fetches (or creates) a cached Mongoose Connection for that slug, and returns a Model bound to that connection.
Real-world Analogy
Imagine a large library where every customer gets their own private bookshelf (Database). You have a standard set of book templates (Mongoose Schemas). When a customer walks in (Request), the librarian (TenantContextManager) identifies them. The tenant-mongoose tool is an assistant that automatically takes your book template, walks over to that specific customer's bookshelf, and interacts only with the books on that shelf, guaranteeing they never accidentally touch another customer's books.
tenant-mongoose only provides request routing and connection caching. It does not handle creating tenant records, parsing subdomains, or creating the physical databases. The consuming application (often using tenant-express and tenant-mongodb) must implement the tenant identification and provisioning lifecycle.2. Prerequisites
Before using tenant-mongoose, your environment must meet the following requirements:
- Node.js: Version 16.x or higher (required for modern ES Modules and AsyncLocalStorage support).
- Package Manager: npm, pnpm, or yarn.
- MongoDB: A running MongoDB instance (v4.4+ recommended).
- Mongoose:
>= 7.0.0(required as a peer dependency for proper schema and connection typing). - @node-tenant/tenant-core:
^0.1.0(required for theTenantContextManagerwhich drives the isolation). - Required Knowledge: Basic understanding of Express (or similar frameworks), standard Mongoose models, and asynchronous JavaScript.
3. Project Structure
The actual internal structure of the tenant-mongoose package is highly focused and minimalistic:
src/index.ts: The single source of truth. It exports the factory functioncreateConnectionPool, and the hooksuseTenantConnectionanduseTenantModel. Developers consume these exports but should never modify this file directly.package.json: Manages dependencies. Note thatmongooseis a peer dependency, meaning your consuming application must provide it.
4. Creating a New Tenant
tenant-mongoose does not automatically create tenants. Tenant creation must be managed by the consuming application (often using tenant-core and tenant-mongodb).However, once a tenant is established in your system, here is how tenant-mongoose integrates with the tenant lifecycle during a request:
Step-by-step Request Flow:
- Tenant Identification: The consuming app uses a middleware (e.g.,
tenantMiddleware) to parse the request (like extracting "acme" fromacme.example.com). - Context Registration: The middleware sets the active tenant in the global
TenantContextManager. - Tenant Initialization in Mongoose: Inside your route handler, you call
useTenantModel('User', UserSchema, getConnection). - Database Resolution:
useTenantModelreads the context, gets the slug "acme", and asks the connection pool for a connection. If it's the first time, it connects tomongodb://.../tenant_acme. - Execution: It returns a standard Mongoose model bound exclusively to
tenant_acme.
import { createConnectionPool, useTenantModel } from '@node-tenant/tenant-mongoose';
import { UserSchema } from './schemas/user';
// 1. Initialize the pool once
const getConnection = createConnectionPool('mongodb://localhost:27017', 'tenant_');
// 2. Inside a request handler (where TenantContextManager is active)
app.post('/api/users', async (req, res) => {
// Retrieves the connection for the current tenant and binds the UserSchema
const User = useTenantModel('User', UserSchema, getConnection);
const user = new User(req.body);
await user.save(); // Saves to the tenant's isolated DB!
res.json(user);
});
5. Database Setup
The database setup revolves around the createConnectionPool function.
- MongoDB Connection: You must provide a base URI without a specific database name (e.g.,
mongodb://localhost:27017). - Tenant Databases: Databases are determined dynamically.
baseUri + '/' + dbPrefix + slug. MongoDB creates these databases automatically the first time a document is inserted. - Connection Lifecycle & Caching:
tenant-mongooseimplements a built-in memory cache (Map<string, Connection>). Connections are reused for subsequent requests from the same tenant, avoiding handshake overhead. - Mongoose Models: Models are compiled lazily and cached on the connection object.
tenant-mongoose does not provide native migration or seeding functionality. Developers must handle schema migrations and data seeding in the consuming application, typically by iterating over all known tenants in the control plane and running scripts against each tenant database.6. Environment Variables
tenant-mongoose does not read environment variables directly from process.env. The consuming application must pass configuration to the package. Below are the standard variables required by your setup:
| Variable | Required | Example | Purpose | Used By |
|---|---|---|---|---|
| MONGODB_URI | Yes | mongodb://localhost:27017 | Base connection string without database name. | App (passed to createConnectionPool) |
| PORT | Optional | 3000 | Port for the web server. | App (Express) |
7. Running Locally
Follow these steps to set up a local development environment using the package:
1. Install Dependencies
This installs the core packages required by your Express app.
npm install @node-tenant/tenant-core @node-tenant/tenant-mongoose mongoose expressAfter execution, verify that node_modules is populated and mongoose is installed without peer dependency warnings.
2. Set Environment Variables
Create a .env file in your project root.
export MONGODB_URI="mongodb://localhost:27017"
export PORT="3000"3. Start the Application
Run your Express server using a local runner like ts-node or tsx.
npx tsx src/index.tsYou should expect the server to bind to port 3000. Connections to MongoDB will be established lazily upon the first request.
4. Testing Tenant Isolation
Use curl with a custom Header to simulate a tenant request (assuming your middleware uses headers for testing).
curl -H "x-tenant-slug: acme" http://localhost:3000/api/usersAfter execution, expect an empty JSON array [], and observe in your MongoDB compass that a database named tenant_acme was successfully created.
8. Deployment Guide
tenant-mongoose does not automatically configure or manage them.When deploying an application utilizing tenant-mongoose to production, follow standard Node.js/MongoDB procedures:
- Production Build: Compile your TypeScript code using
tscor a bundler. - Environment: Securely inject
MONGODB_URIinto the environment. Ensure the database user has permissions to create databases. - Process Management: Use PM2 or Docker to run your compiled Node.js application.
tenant-mongoose's connection pool lives in memory per-process. In clustered environments, each process will maintain its own connection pool. - Reverse Proxy: Configure Nginx or an API Gateway to forward requests to your Node.js application.
9. DNS Configuration
tenant-mongoose operates strictly at the database layer. Hostname parsing and tenant-to-subdomain resolution must be implemented by the consuming application (e.g., using tenant-express).If your application uses subdomains for tenant identification, you must configure a Wildcard DNS record:
Type: CNAME (or A record)
Name: *
Target: your-app-server.comHow it works: If a user visits tenant1.example.com, DNS routes it to your server. Your Express middleware parses tenant1 and sets it in context. tenant-mongoose then reads the context and routes queries to tenant_tenant1.
10. Verifying the Deployment
Use the following procedure to verify your production deployment.
1. Verify API Health
Test a public health endpoint (assuming you implemented one).
curl http://api.example.com/health2. Verify Tenant Creation & Isolation
Create a test record for tenant_a.
curl -X POST -H "Host: tenant-a.example.com" -d '{"name":"Test"}' http://example.com/api/usersVerify that tenant_b cannot see tenant_a's data.
curl -H "Host: tenant-b.example.com" http://example.com/api/usersAfter execution, expect the second command to return an empty array, proving physical database isolation.
11. Common Problems
| Symptom | Cause | Solution |
|---|---|---|
Throws TenantContextNotFoundError | useTenantModel was called outside of an active tenant request lifecycle. | Ensure your route is wrapped in tenantMiddleware before utilizing Mongoose models. |
| Cannot overwrite model once compiled | Registering the same model schema multiple times on a connection directly. | Always use useTenantModel which safely manages caching and avoids overwrite errors. |
| Database fails to create | MongoDB URI includes a database name, breaking dynamic routing. | Ensure MONGODB_URI is just the cluster path without a trailing database name. |
| Excessive connection overhead | Calling createConnectionPool inside a route handler instead of globally. | Initialize the pool factory once at startup and reuse the instance. |
12. Best Practices
Database & Security
- Least-Privilege: Provide MongoDB credentials that can create/delete databases but not modify admin configurations.
- Backups:
tenant-mongoosedoes not handle backups. Configure mongodump at the infrastructure level. - Isolation: Never pass tenant slugs from user input payloads; always derive them securely from server-resolved context (e.g. JWT or subdomain).
Performance & Reliability
- Connection Reuse: Initialize
createConnectionPoolglobally to utilize caching. - Schema Definition: Define schemas outside request handlers to prevent memory leaks and unnecessary instantiation overhead.
- Transactions: Supported inherently. Use
const conn = useTenantConnection()and callconn.startSession().
13. Internal Workflow
The following diagram illustrates the exact internal request lifecycle handled by the architecture:
flowchart TD
A[Client Request] --> B[TenantMiddleware / Auth]
B --> C[TenantContextManager identifies 'slug']
C --> D[Express Route Handler]
D --> E["useTenantModel('User', UserSchema, pool)"]
E --> F{ConnectionPool has slug?}
F -- Yes --> G[Return Cached Connection]
F -- No --> H[mongoose.createConnection]
H --> I[Cache Connection in Map]
I --> G
G --> J[conn.model('User')]
J --> K[Database Query on tenant_slug]
K --> L[Tenant-specific Response]Explanation: The request enters and the middleware (application responsibility) determines the tenant. tenant-mongoose steps in at useTenantModel, retrieves the cached connection for that specific tenant, and executes the Mongoose query against the physically isolated tenant database.
14. FAQs
Does every tenant have a separate MongoDB database?
Yes. tenant-mongoose generates a dedicated connection string per tenant slug (e.g. tenant_acme), guaranteeing complete data isolation.
Does the package automatically create databases?
MongoDB naturally creates databases on the first write. However, for formal provisioning, use tenant-mongodb or handle it in your application layer.
Does it provide migrations or seed scripts?
No. tenant-mongoose only handles query routing. Migrations must be built into the consuming application.
Does it configure DNS, PM2, or SSL?
No. It operates strictly at the application/database layer. Infrastructure configuration is entirely your responsibility.
What happens if a tenant database is unavailable?
Mongoose will throw a connection error or timeout during the request, which should be caught by your global error handler.
15. Deployment Checklist
Copy this checklist into your issue tracker before going to production:
- [ ] Node.js version verified (v16+)
- [ ] Dependencies installed
- [ ] Production environment variables configured (MONGODB_URI)
- [ ] MongoDB configured
- [ ] MongoDB credentials secured (Least Privilege)
- [ ] Tenant configuration verified
- [ ] Database connectivity verified
- [ ] Tenant creation tested (via consuming app logic)
- [ ] Tenant isolation tested (cross-tenant data checks)
- [ ] Application build completed (TypeScript compiled)
- [ ] PM2 configured
- [ ] Reverse proxy configured
- [ ] SSL configured
- [ ] DNS configured
- [ ] Wildcard DNS configured if required
- [ ] APIs verified
- [ ] Logs verified
- [ ] Backups configured (mongodump/Atlas)
- [ ] Restore process tested
- [ ] Production security verified
- [ ] Final tenant request tested