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: paste the integration agent below (or the one on your stack page) into an assistant that can edit your repo (Claude Code, Cursor, …). It assesses your app first, picks the strategy with you, integrates with your existing models and auth, and finishes with an isolated leak test - with fail-closed isolation, you verify, you don't trust.
Start here - one prompt for any stack
New app or existing, and not sure which strategy fits? Select your stack below to generate a custom, fail-closed integration prompt for your AI coding assistant (Claude, Cursor, Copilot, etc.). It enforces database capability rules automatically and includes optional modules like testing, background jobs, and resolution.
Prompt generator
Pick your stack. The generator enforces capability rules and compiles a comprehensive, end-to-end integration prompt with exact API signatures, code examples, and documentation links for your AI coding assistant.
# TenancyJS Integration Agent
You are an integration agent. Add fail-closed multi-tenancy to THIS project
with TenancyJS. Integrate with whatever already exists — do NOT assume a
greenfield app.
**CRITICAL:** Do NOT invent or assume API names, option values, or enum strings
from memory. Your training data may be stale. Verify against:
- The installed package's own TypeScript types (in node_modules)
- The linked documentation pages below
- `npm view <pkg> version` for versions
Read ALL of these before writing any code (source of truth):
- https://tenancyjs.pages.dev/docs (mental model + THE RULES)
- https://tenancyjs.pages.dev/docs/concepts/limitations (what is REJECTED — read first)
- https://tenancyjs.pages.dev/docs/concepts/capability-matrix (what is proven)
- https://tenancyjs.pages.dev/docs/strategies/row-level
- https://tenancyjs.pages.dev/docs/adapters/prisma
- https://tenancyjs.pages.dev/docs/integrations/express
- https://tenancyjs.pages.dev/docs/guides/resolving-tenants
- https://tenancyjs.pages.dev/docs/guides/testing-isolation
## THE RULES (violating these = cross-tenant data leak)
- ✅ Run EVERY tenant query through the scoped client inside `manager.runWithTenant()`
- ✅ Let TenantContextError throw — it's the safety net, don't catch-and-ignore
- ❌ NEVER reach for the native client/model/connection inside a scope —
on facade-enforced tiers that's a direct cross-tenant leak
- ❌ NEVER expect raw SQL or nested writes from the facade — they're REJECTED
unless in a database-enforced scope (database-per-tenant or forced-RLS on PostgreSQL)
where `unrestricted()` gives the real client safely
- ❌ NEVER nest a scope for a different tenant inside an active tenant scope
## Step 0 — Assess the app (change nothing yet)
- Confirm Node.js >= 24
- Framework: **Express**
- ORM: **Prisma**
- Database: **PostgreSQL**
- Strategy: **Row-level (single database)**
- Inventory all data models. Mark each as **tenant-scoped** or **central/global**.
Only tenant-scoped models get registered with the adapter.
- Detect how user identity / session is determined per request (needed for authorize).
- Detect any EXISTING tenant concept (tenantId/orgId column, workspaces table) and REUSE it.
- Report the versions of all packages involved.
## Step 1 — Install & Scaffold
Reference installation guide: https://tenancyjs.pages.dev/docs/getting-started/installation
1. Install the necessary packages:
```bash
npm install tenancyjs-core tenancyjs-adapter-prisma tenancyjs-integration-express tenancyjs-identifiers
```
2. Scaffold the config and middleware using the CLI `init` command (highly recommended):
```bash
npx tenancyjs-cli init --framework express --orm prisma --strategy row-level
```
This scaffolds `tenancy.config.ts`, tenant registry integration hooks, and request middleware. Review the generated files under `src/tenancy/` or `lib/tenancy/` before customizing.
## Step 2 — Create the TenancyManager (one shared instance)
```ts
import { TenancyManager } from "tenancyjs-core";
interface Tenant { readonly id: string; /* your fields */ }
export const manager = new TenancyManager<Tenant>();
```
The manager uses AsyncLocalStorage. Tenant records are frozen inside a scope.
Bootstrappers revert in reverse order on every path (including errors) via try/finally.
## Step 3 — Wire the adapter
### Prisma row-level on PostgreSQL (two paths)
**Path A — Facade extension (simpler, facade-enforced):**
```ts
import { createPrismaAdapter } from "tenancyjs-adapter-prisma";
const adapter = createPrismaAdapter({
manager,
tenantModels: { Order: {}, Post: {} }, // each key = Prisma model name
});
const db = new PrismaClient().$extends(adapter.extension);
```
Raw SQL, nested writes/reads are REJECTED. Use the scoped model API only.
**Path B — RLS-backed (database-enforced, recommended for production):**
```ts
import { createPrismaRowLevelTenancy } from "tenancyjs-adapter-prisma";
const tenancy = createPrismaRowLevelTenancy({
manager,
client, // PrismaClient with @prisma/adapter-pg driver adapter
tables: [{ model: "post", table: "posts", tenantColumn: "tenant_id" }],
});
await tenancy.validate(); // checks ENABLE + FORCE RLS and policy at startup
```
Usage: `await tenancy.run(async (tx) => tx.post.findMany())`
Full raw SQL is safe here — the database enforces isolation via forced RLS.
**Required RLS DDL (apply via `tenancy policy --apply` or manually):**
```sql
CREATE ROLE app_runtime LOGIN NOSUPERUSER NOBYPASSRLS;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
ALTER TABLE posts FORCE ROW LEVEL SECURITY;
CREATE POLICY posts_tenant_isolation ON posts
USING (current_setting('tenancyjs.is_central', true) = 'true'
OR tenant_id = nullif(current_setting('tenancyjs.tenant_id', true), ''))
WITH CHECK (current_setting('tenancyjs.is_central', true) = 'true'
OR tenant_id = nullif(current_setting('tenancyjs.tenant_id', true), ''));
GRANT SELECT, INSERT, UPDATE, DELETE ON posts TO app_runtime;
```
### Generating & Applying RLS Policies via the CLI (recommended)
Reference policy generator: https://tenancyjs.pages.dev/docs/cli#generate-rls-policy-sql
Instead of writing RLS policies by hand in SQL DDL:
1. Define a privileged `admin` Pool in your `tenancy.config.ts`:
```ts
import { Pool } from "pg";
// In defineTenancyRuntime:
admin: new Pool({ connectionString: process.env.ADMIN_DATABASE_URL }),
```
2. Generate and apply RLS policies automatically to your database:
```bash
npx tenancyjs-cli policy --table <t1> --table <t2> --role app_runtime --apply
```
## Step 4 — Wire the framework integration
### Express integration
```ts
import { createExpressTenancyMiddleware } from "tenancyjs-integration-express";
app.use(createExpressTenancyMiddleware({
manager,
resolver, // a TenantResolutionChain — NOT a function
principal: (req) => req.user, // optional
}));
```
## Step 5 — Scope data access
- Route ALL tenant queries through the scoped client (drop manual WHERE tenant_id = …)
- Keep admin/cross-tenant work in explicit central context:
`await manager.runInCentralContext(() => db.tenant.findMany())`
- Add error handling for resolution failures:
use `describeTenantResolutionFailure(outcome.status)` → `{ status, message }`
## Conformance & adversarial testing (Harness Guide)
Reference testing-isolation guide: https://tenancyjs.pages.dev/docs/guides/testing-isolation
Write a two-tenant adversarial test on a real test database (never app data) that plugs into the official tenancyjs-testing contract suite:
```ts
// Implement the adapter conformance testing contract:
import { createRowLevelAdapterContract } from "tenancyjs-testing";
const contractCases = createRowLevelAdapterContract(async () => {
// Propose a harness satisfying RowLevelAdapterContractHarness:
return {
async reset() {
// Wipes/truncates the orders and posts test tables
await db.order.deleteMany();
await db.post.deleteMany();
},
async seed(records) {
// Bulk inserts RowLevelAdapterContractRecord[] to test tables
for (const r of records) {
await db.post.create({
data: { id: r.id, value: r.value, tenantId: r.tenantId }
});
}
},
async create(input) {
// Inserts a single record and returns RowLevelAdapterContractRecord
const res = await db.post.create({
data: { id: input.id, value: input.value }
});
return { id: res.id, value: res.value, tenantId: res.tenantId };
},
async findMany() {
// Queries all records from test tables
const res = await db.post.findMany();
return res.map(r => ({ id: r.id, value: r.value, tenantId: r.tenantId }));
},
async count() {
// Returns count of records in test tables
return db.post.count();
},
async updateMany(value) {
// Updates all records to have the specified value
const res = await db.post.updateMany({ data: { value } });
return res.count;
},
async deleteMany() {
// Deletes all records from test tables
const res = await db.post.deleteMany();
return res.count;
},
async runWithTenant(tenantId, callback) {
return manager.runWithTenant({ id: tenantId }, callback);
},
async runInCentralContext(callback) {
return manager.runInCentralContext(callback);
},
async transaction(callback) {
// Runs operations inside a database transaction
return db.$transaction(async (tx) => {
// execute operations against tx query handle inside transaction
const operations = {
create: async (input) => {
const res = await tx.post.create({ data: { id: input.id, value: input.value } });
return { id: res.id, value: res.value, tenantId: res.tenantId };
},
findMany: async () => {
const res = await tx.post.findMany();
return res.map(r => ({ id: r.id, value: r.value, tenantId: r.tenantId }));
},
count: async () => tx.post.count(),
updateMany: async (value) => {
const res = await tx.post.updateMany({ data: { value } });
return res.count;
},
deleteMany: async () => {
const res = await tx.post.deleteMany();
return res.count;
}
};
return callback(operations);
});
}
};
});
// Run them using Vitest / Jest:
describe("TenancyJS Adapter Conformance Contract", () => {
for (const c of contractCases) {
it(c.name, () => c.run());
}
});
```
Verify that a query executed outside a tenant scope throws a TenantContextError (fail-closed).
Run `tenancy test:leak --test-file <path>` from the CLI to automate this.
## CLI configuration & diagnostics
Create `tenancy.config.ts` (loaded by Node 24 native type-stripping):
```ts
import { defineTenancyRuntime } from "tenancyjs-core";
export default defineTenancyRuntime({
manager,
store, // powers registry commands (list/create/suspend/…)
adapters: [...], // powers `tenant check` capability reporting
provisioner: {
provision: async (tenant) => { /* CREATE SCHEMA/DATABASE */ },
migrate: async (tenant) => { /* run ORM migrator */ },
deprovision: async (tenant) => { /* DROP — destructive */ },
},
admin, // optional — privileged pg connection for policy --apply
dispose: async () => { /* close connections */ },
});
```
Key commands: `tenancy tenant check`, `tenancy doctor`, `tenancy tenant list`,
`tenancy tenant provision <id>`, `tenancy tenant migrate --all`,
`tenancy policy --table posts --role app_runtime --apply`.
## Step 6 — Verify & deliver
1. Deliver all config files, middleware registrations, and converted routes.
2. Write a two-tenant leak test (against a TEST database — never seed app data)
proving (a) no cross-tenant read and (b) unscoped access throws.
3. Report every file changed and anything that could not be applied.
4. Give a concise rollout plan for existing data (backfill, provisioning, staged rollout).Per-stack integration prompts
Prefer to start from your exact stack? These are complete, imperative, verify-after-each-step prompts - no placeholders. Each assesses the app, integrates with what exists, and finishes with an isolated leak test, pinned to that stack's real adapter API.
More stacks are being turned into full integration-agent prompts. Until yours has a dedicated page, the quick prompts below get an assistant most of the way - it still fetches the linked docs for the exact API. Every adapter and integration page is written to be read by an agent.
Quick prompts
Copy as-is (or adapt). Shorter than the full integration-agent prompts above - they point the assistant at the docs and the rules rather than spelling out every file.
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 tenancyjs-adapter-prisma tenancyjs-integration-express @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. On the facade/extension path raw/nested writes are rejected and only registered
models + scalar filters run. For raw SQL/joins bound to the tenant, use the RLS-backed path instead:
`createPrismaRowLevelTenancy` adds a forced-RLS database backstop and runs tenant work inside
`run(async (tx) => ...)`, where `unrestricted()`-style raw SQL is allowed and enforced by the database
(see the adapter doc). On PostgreSQL row-level, always add the forced RLS policy the adapter doc describes.
Deliver the config, the middleware, the converted routes, and an isolated two-tenant leak test (against a
test database - never seed app data) 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 tenancyjs-cli init` scaffolds AdonisJS + Lucid end to end - use it, then adapt.
Install: npm install tenancyjs-core tenancyjs-adapter-lucid tenancyjs-integration-adonis
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 an isolated two-tenant leak test
(two schemas, against a test database - never seed app data) 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 isolated two-tenant leak test the prompt asks for (against a test database, never your app data) - that's the only thing that actually demonstrates isolation. See Testing isolation and the honest Limitations.