TenancyJS

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:

  1. A strategy - how tenants are kept apart: row-level, schema-per-tenant, or database-per-tenant. Pick one per app.
  2. An adapter - your ORM (Prisma, Knex, Lucid, TypeORM, Sequelize, Drizzle, Mongoose). It enforces the strategy on every query.
  3. 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 TenantContextError outside 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

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.

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.

Wire up your stack

Tenant identity is not authorization. Your app still owns auth. TenancyJS owns exactly one thing: that one tenant's data never becomes another's.

On this page