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).
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.
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 itUnlike 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
mongoddoes not support. - Keep
provisionidempotent -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.