TenancyJS

Build with AI

Copy a prompt, paste it into your AI coding assistant, and let it wire up fail-closed tenant isolation — pointed at the exact docs and the rules that stop it from leaking.

Adding tenant isolation is mechanical but easy to get subtly wrong — the dangerous mistakes are the ones that don't throw. These prompts hand your AI assistant (Claude, Cursor, Copilot, Windsurf, …) the exact docs to read and the fail-closed rules to follow, so it builds the wiring for your stack instead of guessing a leaky shortcut.

How to use it: pick your framework + ORM + strategy, copy the universal prompt, fill the {{placeholders}} using the reference table, and paste it into an assistant that can fetch URLs. Then review the result and run the two-tenant test it writes — with fail-closed isolation, you verify, you don't trust.

The universal prompt

Works for any framework × ORM × strategy. Replace every {{placeholder}} (the reference table tells you what to put where), then paste.

You are adding fail-closed multi-tenancy to my app with TenancyJS (https://tenancyjs.pages.dev).

My stack:
- Framework:  {{FRAMEWORK — Express | Next.js | NestJS | AdonisJS}}
- ORM:        {{ORM — Prisma | Knex | Lucid | TypeORM | Sequelize | Drizzle | Mongoose}}
- Database:   {{DATABASE — PostgreSQL | MySQL | MongoDB}}
- Strategy:   {{STRATEGY — row-level | schema-per-tenant | database-per-tenant}}
- Resolve the tenant from: {{TENANT SOURCE — subdomain | header X-Tenant-Id | JWT claim | path param}}

Before writing ANY code, fetch and read these docs. They are the source of truth — do not guess the API:
- https://tenancyjs.pages.dev/docs  (the mental model: strategy × adapter × integration, and THE RULES)
- https://tenancyjs.pages.dev/docs/getting-started/quickstart  (end-to-end wiring)
- https://tenancyjs.pages.dev/docs/adapters/{{ORM_SLUG}}  (the exact adapter API for my ORM)
- https://tenancyjs.pages.dev/docs/integrations/{{FRAMEWORK_SLUG}}  (binding tenant context to each request)
- https://tenancyjs.pages.dev/docs/strategies/{{STRATEGY_SLUG}}  (how my strategy is enforced)
- https://tenancyjs.pages.dev/docs/guides/resolving-tenants  (resolving the tenant from a request)
- https://tenancyjs.pages.dev/docs/concepts/limitations  (what is REJECTED and must NOT be used)
- https://tenancyjs.pages.dev/docs/guides/testing-isolation  (how to prove isolation)

Install (beta channel):
  npm install tenancyjs-core@beta tenancyjs-adapter-{{ORM_SLUG}}@beta tenancyjs-integration-{{FRAMEWORK_PKG}}@beta {{ORM_PEER}}

Then build, in this order:
1. Create ONE TenancyManager<Tenant> and export it.
2. Create the {{ORM}} adapter with strategy "{{STRATEGY}}", registering my tenant-scoped models/tables.
   Get the exact factory name and options from the adapter doc above.
3. Wire the {{FRAMEWORK}} integration so every request resolves the tenant from {{TENANT SOURCE}} and
   opens a tenant scope (middleware / provider / config — see the integration doc).
4. Convert my data access so every tenant query runs through the scoped client the adapter gives me
   inside a scope. Remove manual "WHERE tenant_id = ?" filters — the adapter injects them.
5. For admin / cross-tenant work, use the CENTRAL scope explicitly (see the integration doc). Never run
   tenant-aware queries with no scope.

Follow these rules EXACTLY — this is the entire point, do not weaken them:
- Fail-closed: outside a valid tenant scope, tenant-aware access MUST throw. Do not add any fallback that
  returns unscoped data.
- Never use the native ORM client / model / connection / collection inside a scope — it bypasses the
  facade (a direct cross-tenant leak on MySQL/MongoDB, which have no database backstop). Use only the
  scoped client.
- Raw SQL and nested/relational writes are REJECTED by the facade. They are safe ONLY in a
  database-per-tenant scope, via the adapter's `unrestricted()` escape hatch (see the adapter doc). Do
  not work around the facade any other way.
- Only registered models/tables and plain scalar filter criteria are allowed.

Deliver:
- The tenancy config/module, the per-request wiring, and the converted data-access code.
- A two-tenant test: create tenant A and tenant B with the SAME primary key, write a row under each, and
  assert that querying as A never returns B's row — and that tenant-aware access with no scope throws.
  Follow the testing-isolation guide.

If any detail isn't in the docs above, FETCH the linked page — never invent an API name.

Fill-in reference

Use these to replace the placeholders.

Framework

Framework{{FRAMEWORK_SLUG}} (docs){{FRAMEWORK_PKG}} (package)
Expressexpressexpress
Next.jsnextjsnext
NestJSnestjsnest
AdonisJSadonisadonis

The docs slug and the package suffix differ for Next.js and NestJS — the doc is /integrations/nextjs but the package is tenancyjs-integration-next; the doc is /integrations/nestjs but the package is tenancyjs-integration-nest. Use each column as labelled.

ORM adapter

ORM{{ORM_SLUG}}{{ORM_PEER}} (peer to install)Strategies
Prismaprisma@prisma/clientrow-level, schema-per-tenant, database-per-tenant
Knexknexknexall three (PostgreSQL)
Lucidlucid@adonisjs/lucidall three on PostgreSQL; database-per-tenant on MySQL
TypeORMtypeormtypeorm + pg/mysql2row-level, schema-per-tenant, database-per-tenant
Sequelizesequelizesequelize + pg/mysql2row-level, schema-per-tenant, database-per-tenant
Drizzledrizzledrizzle-ormrow-level, schema-per-tenant, database-per-tenant
Mongoosemongoosemongooserow-level, database-per-tenant (MongoDB)

{{STRATEGY_SLUG}} is row-level, schema-per-tenant, or database-per-tenant.

Lucid pairs with AdonisJS (tenancyjs-integration-adonis). Schema-per-tenant is PostgreSQL-only, and MySQL row-level is experimental — the capability matrix shows exactly what's tested for your database.

Ready-made examples

Fully-filled prompts you can copy as-is (or adapt).

Express + Prisma, row-level on PostgreSQL

You are adding fail-closed multi-tenancy to my Express + Prisma app on PostgreSQL with TenancyJS,
using the row-level strategy. Resolve the tenant from the subdomain.

Read first (source of truth — do not guess the API):
- https://tenancyjs.pages.dev/docs                              (mental model + THE RULES)
- https://tenancyjs.pages.dev/docs/getting-started/quickstart
- https://tenancyjs.pages.dev/docs/adapters/prisma
- https://tenancyjs.pages.dev/docs/integrations/express
- https://tenancyjs.pages.dev/docs/strategies/row-level
- https://tenancyjs.pages.dev/docs/guides/resolving-tenants
- https://tenancyjs.pages.dev/docs/concepts/limitations         (what is REJECTED)
- https://tenancyjs.pages.dev/docs/guides/testing-isolation

Install: npm install tenancyjs-core@beta tenancyjs-adapter-prisma@beta tenancyjs-integration-express@beta @prisma/client

Build: (1) one exported TenancyManager<Tenant>; (2) the Prisma adapter with strategy "rowLevel",
registering my tenant models; (3) Express middleware that resolves the tenant from req.subdomains and
opens the scope; (4) route all tenant queries through the scoped client (drop manual tenant filters);
(5) central scope for admin work.

Rules (do not weaken): fail-closed — unscoped tenant access must throw; never touch the native Prisma
client inside a scope; raw/nested writes are rejected by the facade; registered models + scalar filters
only. On PostgreSQL row-level, also add the forced RLS policy the adapter doc describes.

Deliver the config, the middleware, the converted routes, and a two-tenant colliding-id test proving no
cross-tenant read and that unscoped access throws. Fetch any linked page rather than inventing an API.

AdonisJS + Lucid, schema-per-tenant on PostgreSQL

You are adding fail-closed multi-tenancy to my AdonisJS + Lucid app on PostgreSQL with TenancyJS, using
the schema-per-tenant strategy (one Postgres schema per tenant). Resolve the tenant from a header.

Read first (source of truth — do not guess the API):
- https://tenancyjs.pages.dev/docs                              (mental model + THE RULES)
- https://tenancyjs.pages.dev/docs/getting-started/quickstart
- https://tenancyjs.pages.dev/docs/adapters/lucid
- https://tenancyjs.pages.dev/docs/integrations/adonis
- https://tenancyjs.pages.dev/docs/strategies/schema-per-tenant
- https://tenancyjs.pages.dev/docs/concepts/limitations         (what is REJECTED)
- https://tenancyjs.pages.dev/docs/guides/testing-isolation

Tip: `npx tenancy init` scaffolds AdonisJS + Lucid end to end — use it, then adapt.
Install: npm install tenancyjs-core@beta tenancyjs-adapter-lucid@beta tenancyjs-integration-adonis@beta

Build: (1) config/tenancy.ts with a TenancyManager<Tenant> and the Lucid adapter, strategy
"schemaPerTenant", schema: (tenant) => `tenant_${tenant.id}`; (2) register the integration provider +
middleware; (3) my Lucid models are then scoped automatically — no per-query tenant filters; (4) use the
central context for admin work.

Rules (do not weaken): fail-closed — unscoped tenant access must throw; never reach for the raw Lucid
connection inside a scope; raw queries and nested writes are rejected on the facade (they only work in a
database-per-tenant scope via scope.unrestricted()); registered models + scalar filters only.

Deliver config/tenancy.ts, the provider/middleware registration, and a two-tenant colliding-id test
(two schemas) proving no cross-tenant read and that unscoped access throws. Fetch any linked page rather
than inventing an API.

AI-generated wiring is a starting point, not proof. Always run the two-tenant colliding-id test the prompt asks for — that's the only thing that actually demonstrates isolation. See Testing isolation and the honest Limitations.

On this page