tenancyjs-core
v0.1.2
Published
Framework-neutral tenant context and lifecycle primitives for TenancyJS.
Maintainers
Readme
tenancyjs-core
The foundation of TenancyJS — a fail-closed, TypeScript-first multi-tenancy toolkit for Node.js. Tenant identity follows your async scope, and any tenant-aware access without a valid context throws instead of returning another tenant's data.
📚 Full documentation: tenancyjs.pages.dev — start here.
tenancyjs-core gives you the framework-neutral tenant context, lifecycle, and adapter contract. You
compose it with an adapter (your ORM) and an integration (your framework):
tenancyjs-core × an adapter (Prisma/Knex/Lucid/TypeORM/Sequelize/Drizzle/Mongoose) × an integration (Express/Next.js/NestJS/AdonisJS)Install
npm install tenancyjs-core
# …then add the adapter for your ORM and the integration for your framework, e.g.:
npm install tenancyjs-adapter-prisma tenancyjs-integration-expressTenancyJS is 0.x: safe for real use, with the API still settling before 1.0.
The CLI — tenancyjs-cli
One operational CLI for your whole tenant lifecycle. Run any command with npx tenancyjs-cli … (no install),
or npm install -g tenancyjs-cli for a global tenancy. Every command takes --json for
machine-readable, secret-redacted output.
Scaffold & inspect
npx tenancyjs-cli init # detect your framework + ORM and scaffold the wiring
npx tenancyjs-cli doctor # inspect a project's static setup + migration effort
npx tenancyjs-cli tenant check # health-probe the runtime and warn on untested combinations
npx tenancyjs-cli test:leak --test-file ./leak.mjs # run a cross-tenant isolation leak testManage tenants (backed by your own store — see Configuration)
npx tenancyjs-cli tenant list # list tenants from your store
npx tenancyjs-cli tenant show acme # show one tenant
npx tenancyjs-cli tenant create acme --set plan=pro --set region=eu
npx tenancyjs-cli tenant suspend acme # / tenant activate acmeProvision, migrate & run (per-tenant placement + scripts)
npx tenancyjs-cli tenant provision acme # create the tenant's schema/database (your hook)
npx tenancyjs-cli tenant migrate --all # migrate every tenant, reporting each outcome
npx tenancyjs-cli tenant deprovision acme # drop it (explicit id only — never --all)
npx tenancyjs-cli run ./backfill.ts --tenant acme # run a script inside a tenant scope
npx tenancyjs-cli run ./rollup.ts --central # …or in the central (cross-tenant) scopeFull reference: CLI docs →.
Or let an AI do it: copy a prompt from Build with AI and paste it into your assistant.
How it works (30 seconds)
- An integration resolves the tenant per request and opens a scope.
- Your code queries through the adapter's scoped client — no manual
WHERE tenant_id. - Outside a valid scope, tenant-aware access throws instead of leaking.
import { TenancyManager, type TenantRecord } from "tenancyjs-core";
interface Tenant extends TenantRecord {
readonly id: string;
}
const manager = new TenancyManager<Tenant>();
await manager.runWithTenant({ id: "acme" }, async () => {
// Every tenant query inside here is scoped to "acme".
const tenant = manager.getTenantOrFail();
});
// Outside a scope? It fails closed.
manager.getTenantOrFail(); // ✗ throws TenantContextError — never an unscoped readFull end-to-end wiring is in the Quickstart.
Where to next
| Guide | | | -------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | Getting started | Install + scaffold for your stack | | Adapters | Prisma · Knex · Lucid · TypeORM · Sequelize · Drizzle · Mongoose | | Integrations | Express · Next.js · NestJS · AdonisJS | | Strategies | row-level · schema-per-tenant · database-per-tenant | | The rules & limitations | What's rejected, and why — read before you build |
Core API reference
The primitives this package provides directly.
Guarantees
- Tenant identity is scoped with Node.js
AsyncLocalStorage, never process-global mutable state. - Missing tenant context fails with a typed
TenantContextError. - Central execution is explicit and lexical.
- Nested and concurrent scopes restore their parent automatically.
- Bootstrappers set up in registration order and revert in reverse order.
- Cleanup continues after failures and reports every cleanup error.
Explicit central context
await manager.runInCentralContext(async () => {
// Central (cross-tenant) work is explicit. getTenantOrFail() throws with reason "central" here.
});Resolver failure never enters central mode — framework integrations decide whether a route is central before calling this API.
Bootstrappers and lifecycle events
Bootstrappers are fixed when the manager is created; each is { id, bootstrap, revert }:
const manager = new TenancyManager({
bootstrappers: [
{
id: "database",
async bootstrap(context) {
/* prime context-local resources for context.tenant */
},
async revert(context) {
/* release only resources owned by this scope — always runs on teardown */
},
},
],
});Lifecycle order: tenancy.initializing → bootstrap (registration order) → tenancy.initialized →
callback → tenancy.ending → revert (reverse order) → tenancy.ended. Subscribe with
manager.on(event, listener); the returned function unsubscribes idempotently.
Isolation strategy intent
import { defineConfig } from "tenancyjs-core";
export default defineConfig({ strategy: "rowLevel" });Core carries strategy intent; adapters implement the actual database isolation.
Errors
TenantContextError— tenant access with no scope, or in central mode.InvalidTenantError— a tenant lacks a non-empty stringid.InvalidBootstrapperError/DuplicateBootstrapperError— invalid manager configuration.TenancyLifecycleError— cleanup failed; inspectprimaryError,hasPrimaryError,cleanupErrors.
License
MIT
