TenancyJS
GuidesProvisioning per ORM

Mongoose - provisioning

Ensure each tenant's indexes and database exist with Mongoose, driven by the tenancy CLI.

This recipe implements the provisioner hooks for Mongoose so tenancy tenant provision | migrate | deprovision prepare and drop each tenant's placement. Routing (leasing the tenant's connection at request time) is on the Mongoose adapter page; this page is only the setup seam TenancyJS leaves to you.

MongoDB has no CREATE DATABASE DDL - a database and its collections spring into existence on the first write. So "provisioning" a Mongo tenant is not creating a database; it is opening the tenant's connection and ensuring indexes exist. Every tenant connection must reach a replica set (the adapter requires it for managed transactions).

provisioning.ts
import mongoose from "mongoose";
import { PostSchema } from "./models";

// Per-tenant connection string. With shared credentials this is a routing
// guarantee only; see the note about per-database credentials below.
const uriForTenant = (id: string) =>
  `${process.env.MONGODB_BASE_URL}/tenant_${id}?replicaSet=rs0`;

// Open a tenant connection, ensure its indexes, close it.
async function syncTenantIndexes(id: string): Promise<void> {
  const conn = mongoose.createConnection(uriForTenant(id));
  await conn.asPromise();
  await conn.model("Post", PostSchema).syncIndexes();
  await conn.close();
}

Database-per-tenant

A separate MongoDB database per tenant. There is nothing to create - the database materializes on first write - so provision opens the tenant connection and syncs indexes, and deprovision drops the whole database.

tenancy.config.ts
import { defineTenancyRuntime } from "tenancyjs-core";

export default defineTenancyRuntime({
  manager,
  store,
  adapters: [tenancy], // createMongooseTenancy({ strategy: "databasePerTenant", ... }) - see the adapter page
  provisioner: {
    provision: async (tenant) => {
      // No DDL: just ensure the tenant's indexes exist on its own database.
      await syncTenantIndexes(tenant.id);
    },
    migrate: async (tenant) => {
      // Mongo has no schema DDL. "Migrating" is re-syncing indexes and running
      // your own data-migration scripts against the tenant connection.
      await syncTenantIndexes(tenant.id);
    },
    deprovision: async (tenant) => {
      const conn = mongoose.createConnection(uriForTenant(tenant.id));
      await conn.asPromise();
      await conn.dropDatabase();
      await conn.close();
    },
  },
});

Row-level (shared database)

In row-level mode every tenant shares one database and the same collections, isolated by the tenant field the adapter injects. There is nothing to provision per tenant - no database, no schema, no separate indexes. Ensure the shared collections' indexes once (including any partial or compound index on the tenant field), and leave the provisioner hooks as no-ops or omit them.

provisioner: {
  provision: async () => {}, // shared database - nothing per-tenant to create
},

Run the flow

npx tenancy tenant create acme --set plan=pro   # 1. record + placement
npx tenancy tenant provision acme               # 2. ensure the tenant's indexes
npx tenancy tenant migrate acme                 # 3. re-sync indexes / data migrations
npx tenancy test:leak --test-file ./leak.mjs    # 4. prove isolation before trusting it

Unlike the SQL ORMs, provision here means "ensure indexes exist", not "create a database" - the database appears on first write. Roll a change out to everyone with npx tenancy tenant migrate --all - it reports each tenant's outcome and exits non-zero if any fail, so it's safe in CI.

Notes

  • Replica set required. Every tenant connection must reach a replica set - the adapter runs each operation inside a session-bound transaction, which standalone mongod does not support.
  • Keep provision idempotent - syncIndexes() is safe to repeat, so a half-onboarded tenant is always safe to re-provision, and the CLI may retry.
  • Database-per-tenant with shared credentials is a routing guarantee, not a server-side authorization boundary. If MongoDB itself must reject access to a sibling tenant's database, issue per-database credentials scoped to one database instead of one shared user.
  • No schema-per-tenant on Mongo. MongoDB has no SQL schema namespace, so the only isolation modes are row-level (shared database) and database-per-tenant.

On this page