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 { Pool } from "pg";
import { drizzle } from "drizzle-orm/node-postgres";
import {
createDrizzleTenancy,
createPostgresDrizzleBinding,
} from "tenancyjs-adapter-drizzle";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
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()
unrestricted() returns the native Drizzle transaction - full joins, relations, and raw SQL
expressions. 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 (the transaction 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 protected client is the only guard, so it can't let through what
it can't prove is tenant-safe (see Limitations). It's generic - the
binding erases the concrete Drizzle type, so you supply it at the call site:
import { eq, gt } from "drizzle-orm";
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 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 (the transaction 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).