Drizzle
Protected Drizzle isolation for PostgreSQL and MySQL.
tenancyjs-adapter-drizzle supports all three PostgreSQL strategies and MySQL row-level plus
database-per-tenant. It exposes only callback-scoped plain-value CRUD/count methods; native Drizzle
databases, transactions, SQL expressions, joins, relations, and migrations stay outside the boundary.
Install
npm install tenancyjs-core tenancyjs-adapter-drizzle drizzle-ormAdd pg for PostgreSQL or mysql2 for MySQL.
PostgreSQL row-level
import { drizzle } from "drizzle-orm/node-postgres";
import {
createDrizzleTenancy,
createPostgresDrizzleBinding,
} from "tenancyjs-adapter-drizzle";
const tenancy = createDrizzleTenancy({
manager,
database: createPostgresDrizzleBinding(drizzle({ client: pool })),
tenantTables: [{ table: orders, policyName: "orders_tenant_isolation" }],
});
await tenancy.validate();
const orders = await tenancy.run((client) =>
client.table(orders).findMany({ status: "open" }),
);PostgreSQL row mode validates a non-privileged runtime role, enabled and forced RLS, and the reviewed policy contract before execution unlocks.
Schema and database placement
Schema mode uses an unqualified pgTable and a transaction-local search path:
createDrizzleTenancy({
manager,
database: createPostgresDrizzleBinding(db),
strategy: "schemaPerTenant",
schema: (tenant) => tenant.schemaName,
tenantTables: [{ table: orders }],
});Database mode resolves an opaque key and a cache-owned binding. Supply a close callback when the
binding owns a tenant pool.
MySQL
Use createMySqlDrizzleBinding. Row-level is adapter-enforced and experimental: MySQL has no RLS,
so using the native Drizzle database bypasses TenancyJS. MySQL schemas are databases, therefore
database-per-tenant is the applicable placement strategy and schema-per-tenant is rejected.
Protected operations
The initial surface supports findMany, findOne, count, create, createMany, update, and
delete with plain scalar equality. Tenant filters are composed, writes inject/validate ownership,
and updates cannot move a row to another tenant.
Full query freedom: unrestricted()
In every other strategy the protected client rejects native databases, joins, relations, and raw SQL expressions - 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 that returns the native Drizzle transaction. It's generic - the binding erases the concrete Drizzle type, so you supply it at the call site:
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import * as schema from "./schema";
const report = await tenancy.run(async (client) => {
// The native Drizzle transaction - pass your own db type; full joins, relations, raw SQL.
const trx = client.unrestricted<NodePgDatabase<typeof schema>>();
return trx
.select({ id: schema.orders.id, name: schema.customers.name })
.from(schema.orders)
.innerJoin(schema.customers, eq(schema.customers.id, schema.orders.customerId))
.where(gt(schema.orders.total, 1000));
});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).