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
--jsonoutput, - 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 workstenant 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.tsand everything undersrc/tenancy/orlib/tenancy/) to plain ESM JavaScript.mjs: remove theimport typelines 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 --centralThe script is imported inside the scope, so your normal scoped client just works - no context is
passed in, you import your own db:
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.sqlPrefer 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:
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
| Command | Does |
|---|---|
tenancyjs-cli init | Scaffold config + wiring for your stack |
tenancy tenant check | Health-probe the runtime + warn on untested combos |
tenancy tenant list | List 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 doctor | Inspect 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.