Quick Start
This guide walks you through adding multi-tenancy to an existing Postgres application. By the end, your queries will be automatically scoped to the current tenant via RLS — no WHERE tenant_id = ? needed.
Prerequisites
- A running PostgreSQL 13+ database
- An existing application with tables you want to make tenant-scoped
- Node.js 22+
-
Initialize config
Terminal window npx @usebetterdev/tenant-cli init --database-url $DATABASE_URLThis connects to your database, detects your tables, and creates
better-tenant.config.json:{"tenantTables": ["projects", "tasks"]} -
Generate and apply the migration
Terminal window npx @usebetterdev/tenant-cli migrate -o ./migrationspsql $DATABASE_URL -f ./migrations/*_better_tenant.sqlThis creates the
tenantstable and addstenant_idcolumns, RLS policies, and triggers to each table intenantTables. -
Verify the setup
Terminal window npx @usebetterdev/tenant-cli check --database-url $DATABASE_URLThe check command runs 10+ validations to confirm RLS is correctly configured.
-
Seed a test tenant
Terminal window npx @usebetterdev/tenant-cli seed --name "Acme Corp" --database-url $DATABASE_URL -
Wire up the tenant instance
import { drizzle } from "drizzle-orm/node-postgres";import { Pool } from "pg";import { Hono } from "hono";import { betterTenant } from "@usebetterdev/tenant";import { drizzleDatabase } from "@usebetterdev/tenant/drizzle";import { createHonoMiddleware } from "@usebetterdev/tenant/hono";import { projectsTable } from "./schema"; // your Drizzle table definitionconst pool = new Pool({ connectionString: process.env.DATABASE_URL });const db = drizzle(pool);const tenant = betterTenant({database: drizzleDatabase(db),tenantResolver: { header: "x-tenant-id" },});const app = new Hono();app.use("*", createHonoMiddleware(tenant));app.get("/projects", async (c) => {const db = tenant.getDatabase();const projects = await db.select().from(projectsTable);return c.json(projects);});import { drizzle } from "drizzle-orm/node-postgres";import { Pool } from "pg";import express from "express";import { betterTenant } from "@usebetterdev/tenant";import { drizzleDatabase } from "@usebetterdev/tenant/drizzle";import { createExpressMiddleware } from "@usebetterdev/tenant/express";import { projectsTable } from "./schema"; // your Drizzle table definitionconst pool = new Pool({ connectionString: process.env.DATABASE_URL });const db = drizzle(pool);const tenant = betterTenant({database: drizzleDatabase(db),tenantResolver: { header: "x-tenant-id" },});const app = express();app.use(createExpressMiddleware(tenant));app.get("/projects", async (req, res) => {const db = tenant.getDatabase();const projects = await db.select().from(projectsTable);res.json(projects);});lib/tenant.ts 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" },});// app/api/projects/route.tsimport { withTenant } from "@usebetterdev/tenant/next";import { tenant } from "@/lib/tenant";import { projectsTable } from "@/schema"; // your Drizzle table definitionexport const GET = withTenant(tenant, async (request) => {const db = tenant.getDatabase();const projects = await db.select().from(projectsTable);return Response.json(projects);}); -
Test it
Terminal window curl -H "x-tenant-id: <your-tenant-uuid>" http://localhost:3000/projectsThe response contains only projects belonging to that tenant. No
WHEREclause needed — RLS handles it.
What just happened?
- The CLI generated SQL that created a
tenantstable and added RLS policies to your tables. - Your app resolves the tenant ID from the
x-tenant-idheader on each request. - The Drizzle adapter opens a transaction and runs
SELECT set_config('app.current_tenant', '<uuid>', true)— the function form ofSET LOCAL. - RLS automatically filters every query to the current tenant’s rows.
- When the transaction commits, the session variable is cleared — safe for connection pooling.
Next steps
- Configuration — resolver strategies, tenant API, admin operations
- Framework Adapters — detailed usage for each framework
- CLI & Migrations — all CLI commands and workflows