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.

Warning: Crucial Distinction: 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 the TenantContextManager which 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:

packages/tenant-mongoose/
├── src/
└── index.ts // Core logic (createConnectionPool, useTenantModel)
├── package.json // Dependencies and peer dependency definitions
├── tsup.config.ts // Build configuration for bundling
└── README.md // High-level package overview
  • src/index.ts: The single source of truth. It exports the factory function createConnectionPool, and the hooks useTenantConnection and useTenantModel. Developers consume these exports but should never modify this file directly.
  • package.json: Manages dependencies. Note that mongoose is a peer dependency, meaning your consuming application must provide it.

4. Creating a New Tenant

Note: 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:

  1. Tenant Identification: The consuming app uses a middleware (e.g., tenantMiddleware) to parse the request (like extracting "acme" from acme.example.com).
  2. Context Registration: The middleware sets the active tenant in the global TenantContextManager.
  3. Tenant Initialization in Mongoose: Inside your route handler, you call useTenantModel('User', UserSchema, getConnection).
  4. Database Resolution: useTenantModel reads the context, gets the slug "acme", and asks the connection pool for a connection. If it's the first time, it connects to mongodb://.../tenant_acme.
  5. Execution: It returns a standard Mongoose model bound exclusively to tenant_acme.
typescript
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-mongoose implements 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.
Warning: 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:

VariableRequiredExamplePurposeUsed By
MONGODB_URIYesmongodb://localhost:27017Base connection string without database name.App (passed to createConnectionPool)
PORTOptional3000Port 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.

bash
npm install @node-tenant/tenant-core @node-tenant/tenant-mongoose mongoose express

After 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.

bash
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.

bash
npx tsx src/index.ts

You 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).

bash
curl -H "x-tenant-slug: acme" http://localhost:3000/api/users

After execution, expect an empty JSON array [], and observe in your MongoDB compass that a database named tenant_acme was successfully created.

8. Deployment Guide

Note: PM2, Nginx, Docker, and SSL are deployment/process-management tools used by the consuming application. tenant-mongoose does not automatically configure or manage them.

When deploying an application utilizing tenant-mongoose to production, follow standard Node.js/MongoDB procedures:

  1. Production Build: Compile your TypeScript code using tsc or a bundler.
  2. Environment: Securely inject MONGODB_URI into the environment. Ensure the database user has permissions to create databases.
  3. 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.
  4. Reverse Proxy: Configure Nginx or an API Gateway to forward requests to your Node.js application.

9. DNS Configuration

Note: 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:

text
Type: CNAME (or A record)
Name: *
Target: your-app-server.com

How 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).

bash
curl http://api.example.com/health

2. Verify Tenant Creation & Isolation

Create a test record for tenant_a.

bash
curl -X POST -H "Host: tenant-a.example.com" -d '{"name":"Test"}' http://example.com/api/users

Verify that tenant_b cannot see tenant_a's data.

bash
curl -H "Host: tenant-b.example.com" http://example.com/api/users

After execution, expect the second command to return an empty array, proving physical database isolation.

11. Common Problems

SymptomCauseSolution
Throws TenantContextNotFoundErroruseTenantModel was called outside of an active tenant request lifecycle.Ensure your route is wrapped in tenantMiddleware before utilizing Mongoose models.
Cannot overwrite model once compiledRegistering the same model schema multiple times on a connection directly.Always use useTenantModel which safely manages caching and avoids overwrite errors.
Database fails to createMongoDB URI includes a database name, breaking dynamic routing.Ensure MONGODB_URI is just the cluster path without a trailing database name.
Excessive connection overheadCalling 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-mongoose does 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 createConnectionPool globally 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 call conn.startSession().

13. Internal Workflow

The following diagram illustrates the exact internal request lifecycle handled by the architecture:

mermaid
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:

markdown
- [ ] 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