@usebetterdev/tenant-drizzle
v0.5.4
Published
Drizzle adapter for [@usebetterdev/tenant](https://github.com/usebetter-dev/usebetter). Runs each tenant scope in a transaction with `SET LOCAL app.current_tenant = '<uuid>'` so Postgres RLS applies.
Readme
@usebetterdev/tenant-drizzle
Drizzle adapter for @usebetterdev/tenant. Runs each tenant scope in a transaction with SET LOCAL app.current_tenant = '<uuid>' so Postgres RLS applies.
Install
pnpm add @usebetterdev/tenant drizzle-orm pgUsage
import { drizzle } from "drizzle-orm/node-postgres";
import { betterTenant } from "@usebetterdev/tenant";
import { drizzleDatabase } from "@usebetterdev/tenant/drizzle";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const db = drizzle(pool);
const tenant = betterTenant({
database: drizzleDatabase(db),
tenantResolver: { header: "x-tenant-id" },
});
// tenant.api.createTenant, updateTenant, listTenants, deleteTenant now work (via runAsSystem).
// In handlers: tenant.getDatabase() returns the transaction handle; tenant.getContext().tenant is the loaded Tenant (set loadTenant: false to skip loading).drizzleDatabase(db) bundles the adapter and tenant repository into a single DatabaseProvider. If you use a custom tenants table, pass it via the table option:
import { drizzleDatabase } from "@usebetterdev/tenant/drizzle";
const tenant = betterTenant({
database: drizzleDatabase(db, { table: myCustomTenantsTable }),
tenantResolver: { header: "x-tenant-id" },
});Table shape for tenants must match the CLI-generated schema: id (UUID), name, slug, created_at. Use the exported tenantsTable or pass your own table with the same column contract.
Behaviour
- runWithTenant(tenantId, fn) — Starts a transaction, runs
SELECT set_config('app.current_tenant', tenantId, true), then callsfn(tx). Thetxhandle is the Drizzle transaction; use only this for tenant-scoped tables so RLS sees the tenant. - runAsSystem(fn) — Starts a transaction, runs
SELECT set_config('app.bypass_rls', 'true', true), then callsfn(tx). Use for admin/cron only (e.g. tenant.api.*). RLS policies must allow rows whencurrent_setting('app.bypass_rls', true) = 'true'(CLI generates this bypass policy). - loadTenant — The full Tenant row is loaded by default into
getContext().tenant. SetloadTenant: falseto opt out (saves one query per request). - The handle passed to
fnis the same type as your Drizzle db but transaction-scoped (full Drizzle API ontx).
Testing
Integration tests in src/adapter.integration.test.ts use @testcontainers/postgresql: a Postgres container is started automatically when you run tests (Docker required). If Docker is unavailable, the tests are skipped. You can instead set DATABASE_URL to run the same tests against an existing database.
Run integration tests: From the repo root, run pnpm run test:integration or pnpm run test --filter @usebetterdev/tenant-drizzle. Running pnpm run test from inside packages/core only runs core unit tests; run from root to run all packages (including drizzle integration).
Peer dependency
Requires either pg (node-postgres) or postgres (postgres.js). Install whichever driver you use.
