Skip to content

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, runs SELECT set_config('app.current_tenant', '<uuid>', true), executes fn with the transaction handle, then commits.
  • runAsSystem(fn) — opens a transaction with SELECT 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: timestamp

Peer 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, runs SET LOCAL via $executeRaw, executes fn with the transaction client.
  • runAsSystem(fn) — same pattern with app.bypass_rls = 'true'.

Peer dependencies: @prisma/client (>= 5.0.0)


Framework adapters

Framework adapters are middleware that:

  1. Extract the tenant identifier from the request using your configured resolver
  2. Resolve the identifier to a UUID (slug lookup if needed)
  3. Delegate to the ORM adapter to open a transaction with SET LOCAL
  4. Run your handler inside that transaction

Hono

import { Hono } from "hono";
import { createHonoMiddleware } from "@usebetterdev/tenant/hono";
const app = new Hono();
// Apply to all routes
app.use("*", createHonoMiddleware(tenant));
// Or apply to specific routes
app.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 routes
app.use(createExpressMiddleware(tenant));
// Or apply to specific routes
app.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:

app/api/projects/route.ts
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

ORMFrameworkBest for
DrizzleHonoLightweight APIs, edge-compatible
DrizzleExpressExisting Express apps, REST APIs
DrizzleNext.jsFull-stack React apps
PrismaExpressPrisma-first projects
PrismaNext.jsNext.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