Skip to content

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+
  1. Initialize config

    Terminal window
    npx @usebetterdev/tenant-cli init --database-url $DATABASE_URL

    This connects to your database, detects your tables, and creates better-tenant.config.json:

    {
    "tenantTables": ["projects", "tasks"]
    }
  2. Generate and apply the migration

    Terminal window
    npx @usebetterdev/tenant-cli migrate -o ./migrations
    psql $DATABASE_URL -f ./migrations/*_better_tenant.sql

    This creates the tenants table and adds tenant_id columns, RLS policies, and triggers to each table in tenantTables.

  3. Verify the setup

    Terminal window
    npx @usebetterdev/tenant-cli check --database-url $DATABASE_URL

    The check command runs 10+ validations to confirm RLS is correctly configured.

  4. Seed a test tenant

    Terminal window
    npx @usebetterdev/tenant-cli seed --name "Acme Corp" --database-url $DATABASE_URL
  5. 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 definition
    const 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);
    });
  6. Test it

    Terminal window
    curl -H "x-tenant-id: <your-tenant-uuid>" http://localhost:3000/projects

    The response contains only projects belonging to that tenant. No WHERE clause needed — RLS handles it.

What just happened?

  1. The CLI generated SQL that created a tenants table and added RLS policies to your tables.
  2. Your app resolves the tenant ID from the x-tenant-id header on each request.
  3. The Drizzle adapter opens a transaction and runs SELECT set_config('app.current_tenant', '<uuid>', true) — the function form of SET LOCAL.
  4. RLS automatically filters every query to the current tenant’s rows.
  5. When the transaction commits, the session variable is cleared — safe for connection pooling.

Next steps