TenancyJS

CLI

Inspect, manage, migrate, provision, and run against your live tenants - from one command.

The tenancy CLI (tenancyjs-cli) is how you operate a tenancy. It loads your tenancy.config.ts at runtime - Node 24 strips the types, so there's no transpiler dependency - and acts against your real tenants through your store and hooks. It orchestrates and fails closed, but never invents ORM behaviour it hasn't tested.

npx tenancyjs-cli <command> [--config <path>] [--json]

Every operational command:

  • loads your runtime, resolves tenants through the hardened store,
  • redacts secrets from human and --json output,
  • disposes connections on exit - including on failure,
  • exits non-zero on error (so it's CI-friendly).

Set up a project

npx tenancyjs-cli init          # scaffold config + wiring for your framework and ORM
npx tenancyjs-cli init --framework express --orm drizzle # explicit non-interactive stack
npx tenancyjs-cli init --framework express --orm sequelize --strategy database-per-tenant # pick a strategy
npx tenancyjs-cli tenant check  # verify the runtime loads and the store works

tenant check is the honest one. It reads each adapter's own capability report and warns about any adapter/strategy combination that isn't tested-supported, instead of pretending it's production-ready:

OK   runtime: tenancy config loaded
OK   adapters: 1 adapter(s) configured
WARN adapter:mongoose: schemaPerTenant is reported as "rejected", not
     tested-supported - not in the verified matrix; use at your own risk
OK   store: implements list, find, create
OK   store.list: read 12 tenant(s)

It exits 0 when healthy and 2 when a probe fails.

Init recognizes Express with Prisma, TypeORM, Sequelize, or Drizzle; AdonisJS with Lucid; and Next.js with Prisma, TypeORM, Sequelize, or Drizzle. It previews by default and never overwrites an existing file. On any other framework (Fastify, Koa, Hono, NestJS, …) it exits and asks you to pick a supported one — those stacks wire the core directly; see Any framework.

On a JavaScript (non-TypeScript) project? init currently scaffolds TypeScript (.ts) files. Node.js 24 runs .ts natively (it strips the types), so the scaffolds — and your tenancy.config.ts — work as-is even without a TypeScript toolchain; the CLI loads .ts, .mts, .mjs, and .js configs. If you want plain ESM instead, rename each generated file to .mjs and drop the type annotations, or hand it to your AI assistant. With --ai-context the assistant already has the TENANCY.md guide; then prompt:

Convert the TenancyJS scaffold files (tenancy.config.ts and everything under src/tenancy/ or lib/tenancy/) to plain ESM JavaScript .mjs: remove the import type lines and all type annotations/generics, keep every runtime import and the logic unchanged.

First-class JavaScript scaffolds (--lang js) are on the roadmap.

Manage tenants

Read and write your bring-your-own tenant store.

npx tenancyjs-cli tenant list
npx tenancyjs-cli tenant show acme
npx tenancyjs-cli tenant create acme --set plan=pro --set region=eu
npx tenancyjs-cli tenant suspend acme
npx tenancyjs-cli tenant activate acme

--set key=value is repeatable and is validated before the runtime loads, so a typo fails fast. The id is optional on create if your store generates it.

Run a script in a tenant scope

Run a one-off script - a backfill, an admin task - inside a resolved tenant (or the central) scope. The script's top-level code and its default export run with the active context.

npx tenancyjs-cli run ./backfill.ts --tenant acme
npx tenancyjs-cli run ./rollup.ts --central

The script is imported inside the scope, so your normal scoped client just works - no context is passed in, you import your own db:

backfill.ts
import { db } from "./lib/db";

// Runs scoped to the tenant (or central) the CLI resolved. Top-level code runs;
// a default export, if present, runs too.
await db.order.updateMany({ where: { migrated: false }, data: { migrated: true } });

You must pass exactly one of --tenant <id> or --central; the tenant is resolved (and validated) through your store before the script runs.

Provision & migrate

For schema- and database-per-tenant, these delegate to the provisioner hooks in your config - the CLI never runs an ORM itself. It resolves each tenant's placement from the store record and runs your hook.

npx tenancyjs-cli tenant provision acme     # create the tenant's schema/database
npx tenancyjs-cli tenant migrate acme       # run your migrator for one tenant
npx tenancyjs-cli tenant migrate --all      # ...for every tenant, reporting each outcome
npx tenancyjs-cli tenant deprovision acme   # drop it - explicit id only

--all is only valid for migrate. deprovision always requires an explicit id, so a destructive drop can never fan out across every tenant by accident. A partial --all run reports which tenants failed and exits non-zero.

Generate RLS policy SQL

tenancy policy prints review-ready PostgreSQL forced-RLS DDL for your tenant tables - ENABLE + FORCE ROW LEVEL SECURITY and a <table>_tenant_isolation policy whose USING and WITH CHECK read tenancyjs.tenant_id and tenancyjs.is_central. It prints SQL and executes nothing (it opens no connection), so you review it and apply it with your own migration tool.

npx tenancyjs-cli policy --table posts --table comments --role app_runtime
npx tenancyjs-cli policy --table posts --role app_runtime --tenant-column org_id --out db/rls.sql

Prefer not to hand-run it? Add --apply and the CLI executes the exact same DDL through a privileged admin connection you put on your runtime — so review-then-apply is one command:

tenancy.config.ts
import { Pool } from "pg";
export default defineTenancyRuntime({
  manager,
  // A privileged connection for CLI-applied DDL, separate from your fail-closed runtime role.
  admin: new Pool({ connectionString: process.env.ADMIN_DATABASE_URL }),
});
npx tenancyjs-cli policy --table posts --table comments --role app_runtime --apply

--apply is idempotent (DROP POLICY IF EXISTS + CREATE POLICY) and applies only the RLS contract — it does not create the runtime role (the printed DDL shows the CREATE ROLE for you). Without an admin on the runtime it fails closed.

Every command

CommandDoes
tenancyjs-cli initScaffold config + wiring for your stack
tenancy tenant checkHealth-probe the runtime + warn on untested combos
tenancy tenant listList tenants from your store
tenancy tenant show <id>Show one tenant
tenancy tenant create [<id>] [--set k=v …]Create a tenant
tenancy tenant suspend <id> / activate <id>Change tenant status
tenancy tenant provision <id> / deprovision <id>Create / drop a tenant's placement
tenancy tenant migrate (<id> | --all)Migrate one or all tenants
tenancy run <script> (--tenant <id> | --central)Run a script in a scope
tenancy doctorInspect a project's static setup
tenancy test:leak --test-file <path>Run a cross-tenant isolation leak test
tenancy policy --table <t> --role <role>Generate forced-RLS policy DDL (prints SQL, runs nothing)

Add --json to any of them for machine-readable, still-redacted output.

On this page