Introduction
Fail-closed, TypeScript-first multi-tenancy for Node.js - one tenant-isolation contract for the framework and ORM you already use.
TenancyJS gives every framework and ORM the same tenant-context and isolation contract - without
replacing the framework or ORM you already use. Tenant identity follows the async execution scope, so
your queries stay scoped without threading a tenantId through every call.
And the guarantee that matters:
Any tenant-aware data access without a valid tenant context throws - it never returns another tenant's data.
How it fits together
You compose three independent choices - they don't need to know about each other, so mix and match:
- A strategy - how tenants are kept apart: row-level, schema-per-tenant, or database-per-tenant. Pick one per app.
- An adapter - your ORM (Prisma, Knex, Lucid, TypeORM, Sequelize, Drizzle, Mongoose). It enforces the strategy on every query.
- An integration - your framework (Express, Next.js, NestJS, AdonisJS). It sets the current tenant for each request.
At runtime it's one loop: the integration resolves the tenant and opens a scope → your code queries through the adapter's scoped client → the strategy isolates every query → the scope tears down on the way out. Miss the scope and access throws instead of leaking. That's the whole model.
The rules - what to do, what to avoid
The guarantee only holds for access that goes through the scoped client. So there are a few rules - follow these and you can't leak:
- ✅ Do run every tenant query through the client you get inside
tenancy.run(...)(or your models, on Lucid/AdonisJS), within an open tenant scope. - ✅ Do let it throw. A
TenantContextErroroutside a scope is the safety net doing its job - don't catch-and-ignore it. - ❌ Don't reach for the native client, model, connection, or collection inside a scope - that sidesteps the facade, and on MySQL/MongoDB (no database backstop) it's a direct cross-tenant leak.
- ❌ Don't expect raw SQL or nested/relational writes from the facade - they're rejected unless
you're in a database-per-tenant scope, where
unrestricted()hands you the real client safely.
The full, honest list is on the Limitations page - read it before you build.
New here? Start with these
Build with AI
Copy a prompt, paste it into your AI assistant, and let it wire up isolation for your stack - pointed at the right docs and the fail-closed rules.
Installation
Scaffold your stack with the CLI, or install the packages by hand.
Quickstart
Wire tenant context into an app and run your first scoped query.
Configuration
Define a tenancy.config.ts runtime the CLI and your app share.
Capability matrix
Exactly which adapter × strategy combinations are tested-supported.
The core idea
The dangerous bug in multi-tenancy is the one that doesn't throw. Forget a WHERE tenant_id = ? and
nothing breaks in the demo - you just quietly returned one customer's data to another. TenancyJS makes
the safe path the default: no valid tenant context, no data.
Tenant context
How identity flows through your app on AsyncLocalStorage - and why it fails closed.
Security model
What TenancyJS guarantees, what it doesn't, and where the boundaries are.
Limitations
What the scoped facade rejects - no raw queries, no nested reads/writes - and why.
Choose an isolation strategy
Three strategies, one contract. Pick per app; the same tenant scope drives all three.
On MySQL or MongoDB? Schema-per-tenant is not a distinct strategy there, but row-level and database-per-tenant are available. The capability matrix breaks down the enforcement strength.
Single database (row-level)
Shared tables + tenant_id, enforced by forced Postgres RLS or query-scoping.
Schema per tenant
One Postgres schema per tenant via transaction-local search_path.
Database per tenant
A separate database per tenant, cache-routed for hard isolation.
Wire up your stack
Adapters
Enforce isolation inside your ORM - Prisma, Knex, Lucid, TypeORM, Sequelize, Drizzle, Mongoose.
Integrations
Scope every request automatically - Express, Next.js, NestJS, AdonisJS.
Operational CLI
Inspect, create, migrate, and run against your live tenants.
Tenant identity is not authorization. Your app still owns auth. TenancyJS owns exactly one thing: that one tenant's data never becomes another's.