TenancyJS
Adapters

Knex

All three isolation strategies on PostgreSQL with Knex - wired end to end into any framework.

tenancyjs-adapter-knex is the most complete adapter - it supports all three strategies on PostgreSQL. This page shows row-level with Express end to end, then the other strategies. Different framework? Swap only the integration (see below).

Install

npm install tenancyjs-core tenancyjs-adapter-knex tenancyjs-integration-express knex

Wire it into your app (row-level)

Create the manager + adapter

tenancy.ts
import { TenancyManager } from "tenancyjs-core";
import { createKnexTenancy } from "tenancyjs-adapter-knex";
import Knex from "knex";

export interface Tenant {
  readonly id: string;
}

export const manager = new TenancyManager<Tenant>();
const knex = Knex({ client: "pg", connection: process.env.DATABASE_URL });

export const tenancy = createKnexTenancy({
  manager,
  knex,
  strategy: "rowLevel",
  tenantTables: ["orders", "posts"],
});

On PostgreSQL, add a forced RLS policy to each tenant table (the database backstop).

Bind it to requests

Inside a request, run your queries through tenancy.run - it hands you a tenant-scoped Knex.

server.ts
import express from "express";
import { createExpressTenancyMiddleware } from "tenancyjs-integration-express";
import { manager, tenancy } from "./tenancy";

const app = express();

app.use(
  createExpressTenancyMiddleware({
    manager,
    resolver: (req) => ({ id: req.subdomains.at(-1) ?? "" }),
  }),
);

app.get("/orders", async (_req, res) => {
  const orders = await tenancy.run((db) => db("orders").select("*"));
  res.json(orders); // only the current tenant's rows
});

Schema per tenant

Each tenant gets its own Postgres schema, selected per transaction via search_path. Add a per-tenant role for database-enforced isolation.

export const tenancy = createKnexTenancy({
  manager,
  knex,
  strategy: "schemaPerTenant",
  schema: (tenant) => `tenant_${tenant.id}`,
  // optional: role: (tenant) => `tenant_${tenant.id}_role`,
});

Database per tenant

Route a dedicated connection per tenant, pooled and reused through a bounded, single-flight cache.

export const tenancy = createKnexTenancy({
  manager,
  knex,
  strategy: "databasePerTenant",
  connection: (tenant) => ({
    key: tenant.database,
    create: () => Knex({ client: "pg", connection: tenant.databaseUrl }),
  }),
});

Cross-placement access is rejected, central scope uses the base connection, and every leased connection is disposed on teardown. See Database per tenant.

Full query freedom: unrestricted()

In every other strategy the scoped client rejects raw SQL, joins, and nested queries - it's the only guard, so it can't let through what it can't prove is tenant-safe (see Limitations). Database-per-tenant is different: the leased connection is the tenant's own database, so any query is isolated by construction. There, the client gives you an escape hatch:

const report = await tenancy.run(async (db) => {
  // The raw, tenant-scoped Knex transaction - full joins, raw SQL, nested reads/writes.
  const trx = db.unrestricted();

  return trx("orders")
    .join("customers", "customers.id", "orders.customer_id")
    .whereRaw("orders.total > ?", [1000])
    .select("orders.id", "customers.name");
});

unrestricted() is fail-closed. It returns the real transaction only in a genuinely database-enforced scope - a database-per-tenant config running in tenant mode, where a per-tenant connection was actually leased. It throws in row-level and schema-per-tenant scopes, and even in a database-per-tenant config used in central mode (which runs on the shared admin connection, not a tenant's database). The freedom comes from the connection boundary, never from the config name (ADR-0033).

Use a different framework

Keep the adapter half; only swap the integration import:

On this page