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",
  // A record keyed by table name (value carries optional tenantColumn / policyName):
  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, // a TenantResolutionChain (tenancyjs-identifiers) - see the Express guide
  }),
);

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()

unrestricted() returns the raw, tenant-scoped Knex transaction - full joins, raw SQL, and nested reads/writes. It's available in the two scopes where the database itself keeps a query inside the tenant: database-per-tenant (the leased connection is the tenant's own database) and forced-RLS row-level on PostgreSQL (raw SQL runs under a non-BYPASSRLS role bound by the validated RLS policy, so it cannot cross tenants). It's still refused in schema-per-tenant, central mode, and MySQL row-level (no RLS backstop) - there the scoped client is the only guard, so it can't let through what it can't prove is tenant-safe (see Limitations):

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 a real transaction only where the database itself enforces the tenant boundary - a database-per-tenant config in tenant mode (a per-tenant connection was actually leased), or forced-RLS row-level on PostgreSQL (raw SQL is bound by the validated policy under a non-BYPASSRLS role). It throws in schema-per-tenant scopes, in MySQL row-level (no RLS backstop), and in any config used in central mode (which runs on the shared admin connection, not a tenant's database). The freedom comes from a database boundary, never from the config name (ADR-0038).

Use a different framework

Keep the adapter half; only swap the integration import:

On this page