Database per tenant
A separate database per tenant, routed through a bounded connection cache - the hardest isolation.
Database-per-tenant gives each tenant its own database. It's the strongest isolation - physically separate data, per-tenant backups and restores, no noisy neighbours - and the natural fit when tenants are few and heavy, or when compliance demands separation.
Supported on Knex, Lucid, Prisma, TypeORM, Sequelize, Drizzle, and Mongoose.
Adversarially proven on PostgreSQL, on MySQL through Prisma, TypeORM, Sequelize, and Drizzle, and on MongoDB through Mongoose. MySQL's
SCHEMA and DATABASE are the same namespace, so this is its schema-isolation equivalent.
How it works
You tell the adapter how to build a connection (or client) for a given tenant. TenancyJS routes each tenant scope to its own connection through a bounded, single-flight resource cache:
- Bounded + LRU - connections are pooled and evicted, so you don't exhaust the database.
- Single-flight - concurrent requests for the same tenant share one in-flight connection creation.
- Collision-guarded - a 1:1 key↔resource guard prevents two tenants ever sharing a connection.
- Fail-closed - cross-placement access is rejected; the central scope uses the base connection.
const tenancy = createKnexTenancy({
manager,
knex,
strategy: "databasePerTenant",
connection: (tenant) => ({
key: tenant.database,
create: () => knexFactory(tenant.databaseUrl),
}),
});Prisma uses createPrismaDatabaseTenancy; TypeORM/Sequelize/Drizzle lease tenant ORM resources; Mongoose
leases tenant-bound replica-set connections. See each adapter guide:
Separate storage does not automatically mean server-side authorization. If one credential can reach sibling databases, TenancyJS supplies a protected routing boundary; use per-tenant credentials when the database must independently reject cross-database access.
Full query freedom
Because each tenant scope runs on its own connection, any query is isolated by construction - so this
is the one strategy where the facade's raw/nested/join restrictions don't buy any safety. In a
database-per-tenant scope you can reach for the real, tenant-scoped client and write completely normal
queries (joins, nested reads/writes, raw SQL). Every adapter supports this: client.unrestricted()
on Knex, TypeORM, Sequelize, Drizzle, and Mongoose; scope.unrestricted() on
Lucid; and Prisma database-per-tenant already hands you the raw leased client
directly. It stays fail-closed - it throws unless a per-tenant connection was actually leased (so it
throws in central mode and in the facade-enforced strategies). See
Limitations → two enforcement tiers.
Provisioning
Each tenant's database must be created and migrated before use, and dropped on offboarding - through the CLI's provisioning hooks:
npx tenancy tenant provision acme # create + migrate the database
npx tenancy tenant migrate --all # migrate every tenant's database
npx tenancy tenant deprovision acme # drop it (explicit id only - never --all)Tradeoffs
Hardest isolation, but the most operational overhead - more databases to provision, migrate, monitor, and back up. Every leased connection is disposed on teardown, and the cache keeps the connection count bounded, but plan your database's connection limits accordingly.