Framework Adapters
Better Tenant has two kinds of adapters:
- ORM adapters — handle transactions,
SET LOCAL, and RLS bypass - Framework adapters — middleware that resolves the tenant and delegates to the ORM adapter
ORM adapters
Drizzle
The Drizzle adapter wraps your queries in a transaction with set_config('app.current_tenant', '<uuid>', true) — the function form of SET LOCAL.
import { drizzle } from "drizzle-orm/node-postgres";import { Pool } from "pg";import { betterTenant } from "@usebetterdev/tenant";import { drizzleDatabase } from "@usebetterdev/tenant/drizzle";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });const db = drizzle(pool);
export const tenant = betterTenant({ database: drizzleDatabase(db), tenantResolver: { header: "x-tenant-id" },});drizzleDatabase(db) bundles the adapter and tenant repository into a single database provider. If you use a custom tenants table, construct the provider manually:
import { drizzleAdapter, createGetTenantRepository } from "@usebetterdev/tenant/drizzle";
export const tenant = betterTenant({ database: { adapter: drizzleAdapter(db), getTenantRepository: createGetTenantRepository(myCustomTable), }, tenantResolver: { header: "x-tenant-id" },});The getTenantRepository is required. If you construct the provider manually, you must always include it.
What the adapter does:
runWithTenant(tenantId, fn)— opens a Drizzle transaction, runsSELECT set_config('app.current_tenant', '<uuid>', true), executesfnwith the transaction handle, then commits.runAsSystem(fn)— opens a transaction withSELECT set_config('app.bypass_rls', 'true', true)for admin operations.
Tenant repository:
drizzleDatabase() includes a built-in tenant repository that provides getBySlug for slug-to-UUID resolution and powers the tenant.api CRUD operations.
The tenantsTable is the Drizzle schema for the tenants table created by the CLI migration:
// Auto-created by the migrate command// id: UUID primary key// name: text// slug: text (unique)// createdAt: timestampPeer dependencies: drizzle-orm, pg
Prisma
The Prisma adapter uses interactive transactions with $executeRaw for session variables.
import { PrismaClient } from "@prisma/client";import { betterTenant } from "@usebetterdev/tenant";import { prismaDatabase } from "@usebetterdev/tenant/prisma";
const prisma = new PrismaClient();
export const tenant = betterTenant({ database: prismaDatabase(prisma), tenantResolver: { header: "x-tenant-id" },});What the adapter does:
runWithTenant(tenantId, fn)— opens a Prisma interactive$transaction, runsSET LOCALvia$executeRaw, executesfnwith the transaction client.runAsSystem(fn)— same pattern withapp.bypass_rls = 'true'.
Peer dependencies: @prisma/client (>= 5.0.0)
Framework adapters
Framework adapters are middleware that:
- Extract the tenant identifier from the request using your configured resolver
- Resolve the identifier to a UUID (slug lookup if needed)
- Delegate to the ORM adapter to open a transaction with
SET LOCAL - Run your handler inside that transaction
Hono
import { Hono } from "hono";import { createHonoMiddleware } from "@usebetterdev/tenant/hono";
const app = new Hono();
// Apply to all routesapp.use("*", createHonoMiddleware(tenant));
// Or apply to specific routesapp.use("/api/*", createHonoMiddleware(tenant));
app.get("/api/projects", async (c) => { const ctx = tenant.getContext(); const db = tenant.getDatabase(); const projects = await db.select().from(projectsTable); return c.json(projects);});Express
import express from "express";import { createExpressMiddleware } from "@usebetterdev/tenant/express";
const app = express();
// Apply to all routesapp.use(createExpressMiddleware(tenant));
// Or apply to specific routesapp.use("/api", createExpressMiddleware(tenant));
app.get("/api/projects", async (req, res) => { const ctx = tenant.getContext(); const db = tenant.getDatabase(); const projects = await db.select().from(projectsTable); res.json(projects);});Next.js App Router
Next.js uses a per-route wrapper instead of global middleware:
import { withTenant } from "@usebetterdev/tenant/next";import { tenant } from "@/lib/tenant";
export const GET = withTenant(tenant, async (request) => { const ctx = tenant.getContext(); const db = tenant.getDatabase(); const projects = await db.select().from(projectsTable); return Response.json(projects);});
export const POST = withTenant(tenant, async (request) => { const body = await request.json(); const db = tenant.getDatabase(); await db.insert(projectsTable).values(body); return Response.json({ ok: true }, { status: 201 });});Choosing your stack
| ORM | Framework | Best for |
|---|---|---|
| Drizzle | Hono | Lightweight APIs, edge-compatible |
| Drizzle | Express | Existing Express apps, REST APIs |
| Drizzle | Next.js | Full-stack React apps |
| Prisma | Express | Prisma-first projects |
| Prisma | Next.js | Next.js + Prisma projects |
Next steps
- Configuration — resolver strategies, tenant API, and admin operations
- CLI & Migrations — generate migrations, verify RLS, and seed tenants
- Architecture — how transaction-scoped RLS and bypass work under the hood