CLI & Migrations
The CLI (@usebetterdev/tenant-cli) generates RLS policies and triggers for your tenant-scoped tables. Your ORM manages the schema (tables, columns) — the CLI handles everything else.
Configuration
Section titled “Configuration”The CLI reads from better-tenant.config.json in your project root:
{ "tenantTables": ["projects", "tasks"]}Alternatively, add a "betterTenant" key to your package.json:
{ "betterTenant": { "tenantTables": ["projects", "tasks"] }}tenantTables lists the tables that should be tenant-scoped. The CLI generates RLS policies and triggers for each one.
Commands
Section titled “Commands”init — create config interactively
Section titled “init — create config interactively”Connects to your database, detects tables, and creates better-tenant.config.json. It also detects your ORM (Drizzle or Prisma) and shows tailored next steps:
npx @usebetterdev/tenant-cli init --database-url $DATABASE_URLnpx @usebetterdev/tenant-cli init # prompts for DATABASE_URLNon-interactive mode
Section titled “Non-interactive mode”Pass -n (or --non-interactive) to disable all interactive prompts. This is useful for CI/CD pipelines and LLM coding agents (Claude Code, Cursor, etc.) that cannot respond to prompts. In this mode --tables and --orm are required — the CLI fails fast with a clear error if either is missing.
npx @usebetterdev/tenant-cli init -n \ --tables "projects,tasks,users" \ --orm drizzle| Flag | Description |
|---|---|
-n, --non-interactive | Disable all prompts (requires --tables and --orm) |
--tables <tables> | Comma-separated table names (required with -n) |
--orm <orm> | drizzle or prisma (required with -n) |
--overwrite | Overwrite existing config without prompting |
No database connection is needed in non-interactive mode.
migrate — generate RLS migration
Section titled “migrate — generate RLS migration”Generates SQL for RLS policies on the tenants lookup table (open SELECT, writes require bypass_rls), plus RLS policies, triggers, and the set_tenant_id() function for all tenant-scoped tables in tenantTables:
# Preview the SQLnpx @usebetterdev/tenant-cli migrate --dry-run
# Write to directory (auto-generates timestamped filename)npx @usebetterdev/tenant-cli migrate -o ./rls
# Write to specific filenpx @usebetterdev/tenant-cli migrate -o drizzle/0001_rls.sqlThe generated migration includes:
- RLS policies for the
tenantslookup table:rls_tenants_read(open SELECT) andrls_tenants_write(requiresbypass_rls) set_tenant_id()function that auto-populatestenant_idon INSERTENABLE ROW LEVEL SECURITYandFORCE ROW LEVEL SECURITYon each tenant-scoped table- RLS policy with
USINGandWITH CHECKclauses (tenant isolation + bypass support) set_tenant_id_triggeron each tenant-scoped table
add-table — add RLS to a new table
Section titled “add-table — add RLS to a new table”Generates RLS SQL for a single table. Use this when you add a new tenant-scoped table after initial setup:
# Previewnpx @usebetterdev/tenant-cli add-table comments --dry-run
# Write to directory (auto-generates timestamped filename)npx @usebetterdev/tenant-cli add-table comments -o ./rls
# Write to specific filenpx @usebetterdev/tenant-cli add-table comments -o drizzle/0002_comments_rls.sqlAfter running add-table, add the table name to tenantTables in your config.
check — verify database setup
Section titled “check — verify database setup”Runs 10+ validations against your database to confirm RLS is correctly configured:
npx @usebetterdev/tenant-cli check --database-url $DATABASE_URLChecks include:
tenantstable exists with correct columnstenantstable has RLS enabled, forced, and both policies (rls_tenants_read,rls_tenants_write)tenant_idcolumn exists on each tenant-scoped table- RLS is enabled and forced on each tenant-scoped table
- Policies have correct
USINGandWITH CHECKclauses withbypass_rls set_tenant_id()trigger is attached
seed — insert a test tenant
Section titled “seed — insert a test tenant”Creates a tenant record using runAsSystem (RLS bypass):
npx @usebetterdev/tenant-cli seed --name "Acme Corp" --database-url $DATABASE_URLWorkflow
Section titled “Workflow”Your ORM owns the schema (tables, columns). The CLI generates only RLS policies and triggers.
-
Run
initto createbetter-tenant.config.json -
Add
tenantsTable,tenantId, and.enableRLS()to each tenant-scoped table:import { tenantsTable, tenantId } from "@usebetterdev/tenant/drizzle";export { tenantsTable };export const projectsTable = pgTable("projects", {id: serial("id").primaryKey(),name: text("name").notNull(),...tenantId,}).enableRLS();import { sql } from "drizzle-orm";import { pgTable, serial, text, uuid } from "drizzle-orm/pg-core";import { tenantsTable } from "@usebetterdev/tenant/drizzle";export { tenantsTable };export const projectsTable = pgTable("projects", {id: serial("id").primaryKey(),name: text("name").notNull(),tenantId: uuid("tenant_id").notNull().references(() => tenantsTable.id).default(sql`(current_setting('app.current_tenant', true))::uuid`),}).enableRLS(); -
Generate and apply schema migration:
Terminal window npx drizzle-kit generatenpx drizzle-kit migrate -
Create a custom migration for RLS. The
--prefix=noneflag is required — without it, Drizzle Kit adds a numeric prefix to the filename and the-opath in the next step won’t match:Terminal window npx drizzle-kit generate --custom --name=better_tenant_rls --prefix=none -
Fill the empty migration with RLS SQL. The filename must match exactly what Drizzle Kit generated in the previous step:
Terminal window npx @usebetterdev/tenant-cli migrate -o drizzle/_better_tenant_rls.sql -
Apply the RLS migration:
Terminal window npx drizzle-kit migrate -
Verify setup:
Terminal window npx @usebetterdev/tenant-cli check --database-url $DATABASE_URL
-
Run
initto createbetter-tenant.config.json -
Add the Tenant model to
schema.prisma:model Tenant {id String @id @default(uuid()) @db.Uuidname Stringslug String @uniquecreatedAt DateTime @default(now()) @map("created_at") @db.Timestamptzprojects Project[]@@map("tenants")} -
Add
tenantIdto each tenant-scoped model (and a reverse relation onTenant):tenantId String @map("tenant_id") @db.Uuidtenant Tenant @relation(fields: [tenantId], references: [id]) -
Generate and apply schema migration:
Terminal window npx prisma migrate dev --name setup -
Create a draft migration for RLS using
--create-only:Terminal window npx prisma migrate dev --create-only --name better_tenant_rls -
Replace the empty migration SQL with RLS policies:
Terminal window npx @usebetterdev/tenant-cli migrate \-o prisma/migrations/*_better_tenant_rls/migration.sql -
Apply the RLS migration:
Terminal window npx prisma migrate dev -
Verify setup:
Terminal window npx @usebetterdev/tenant-cli check --database-url $DATABASE_URL
Adding a table later
Section titled “Adding a table later”When you add a new tenant-scoped table after initial setup:
-
Add the table to your Drizzle schema (with
tenant_idcolumn):import { tenantId } from "@usebetterdev/tenant/drizzle";export const commentsTable = pgTable("comments", {id: serial("id").primaryKey(),body: text("body").notNull(),...tenantId,}).enableRLS(); -
Generate and apply the schema migration:
Terminal window npx drizzle-kit generatenpx drizzle-kit migrate -
Create a custom migration for RLS. The
--prefix=noneflag is required — without it, Drizzle Kit adds a numeric prefix and the-opath in the next step won’t match:Terminal window npx drizzle-kit generate --custom --name=comments_rls --prefix=none -
Fill the empty migration with RLS SQL. The filename must match exactly what Drizzle Kit generated in the previous step:
Terminal window npx @usebetterdev/tenant-cli add-table comments -o drizzle/_comments_rls.sql -
Apply the RLS migration:
Terminal window npx drizzle-kit migrate -
Add the table name to
tenantTablesin your config.
-
Add the model to
schema.prisma(withtenantIdfield):model Comment {id Int @id @default(autoincrement())body StringtenantId String @map("tenant_id") @db.Uuidtenant Tenant @relation(fields: [tenantId], references: [id])@@map("comments")}Also add
comments Comment[]to your existingTenantmodel. -
Generate and apply the schema migration:
Terminal window npx prisma migrate dev --name add_comments -
Create a draft RLS migration using
--create-only:Terminal window npx prisma migrate dev --create-only --name comments_rls -
Replace the empty migration SQL with RLS policies:
Terminal window npx @usebetterdev/tenant-cli add-table comments \-o prisma/migrations/*_comments_rls/migration.sql -
Apply the RLS migration:
Terminal window npx prisma migrate dev -
Add the table name to
tenantTablesin your config.
Programmatic API
Section titled “Programmatic API”The CLI exports functions for use in scripts or custom tooling:
import { generateMigrationSql } from "@usebetterdev/tenant-cli/migrate";import { runCheck } from "@usebetterdev/tenant-cli/check";import { runSeed } from "@usebetterdev/tenant-cli/seed";Next steps
Section titled “Next steps”- Configuration — resolver strategies, tenant API, and admin operations
- Framework Adapters — per-adapter setup for Drizzle, Prisma, Hono, Express, and Next.js
- Architecture — how the generated SQL and RLS policies work under the hood