Skip to content

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.

The CLI reads from better-tenant.config.json in your project root:

better-tenant.config.json
{
"tenantTables": ["projects", "tasks"]
}

Alternatively, add a "betterTenant" key to your package.json:

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.

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:

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

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.

Terminal window
npx @usebetterdev/tenant-cli init -n \
--tables "projects,tasks,users" \
--orm drizzle
FlagDescription
-n, --non-interactiveDisable all prompts (requires --tables and --orm)
--tables <tables>Comma-separated table names (required with -n)
--orm <orm>drizzle or prisma (required with -n)
--overwriteOverwrite existing config without prompting

No database connection is needed in non-interactive mode.

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:

Terminal window
# Preview the SQL
npx @usebetterdev/tenant-cli migrate --dry-run
# Write to directory (auto-generates timestamped filename)
npx @usebetterdev/tenant-cli migrate -o ./rls
# Write to specific file
npx @usebetterdev/tenant-cli migrate -o drizzle/0001_rls.sql

The generated migration includes:

  • RLS policies for the tenants lookup table: rls_tenants_read (open SELECT) and rls_tenants_write (requires bypass_rls)
  • set_tenant_id() function that auto-populates tenant_id on INSERT
  • ENABLE ROW LEVEL SECURITY and FORCE ROW LEVEL SECURITY on each tenant-scoped table
  • RLS policy with USING and WITH CHECK clauses (tenant isolation + bypass support)
  • set_tenant_id_trigger on each tenant-scoped table

Generates RLS SQL for a single table. Use this when you add a new tenant-scoped table after initial setup:

Terminal window
# Preview
npx @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 file
npx @usebetterdev/tenant-cli add-table comments -o drizzle/0002_comments_rls.sql

After running add-table, add the table name to tenantTables in your config.

Runs 10+ validations against your database to confirm RLS is correctly configured:

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

Checks include:

  • tenants table exists with correct columns
  • tenants table has RLS enabled, forced, and both policies (rls_tenants_read, rls_tenants_write)
  • tenant_id column exists on each tenant-scoped table
  • RLS is enabled and forced on each tenant-scoped table
  • Policies have correct USING and WITH CHECK clauses with bypass_rls
  • set_tenant_id() trigger is attached

Creates a tenant record using runAsSystem (RLS bypass):

Terminal window
npx @usebetterdev/tenant-cli seed --name "Acme Corp" --database-url $DATABASE_URL

Your ORM owns the schema (tables, columns). The CLI generates only RLS policies and triggers.

  1. Run init to create better-tenant.config.json

  2. 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();
  3. Generate and apply schema migration:

    Terminal window
    npx drizzle-kit generate
    npx drizzle-kit migrate
  4. Create a custom migration for RLS. The --prefix=none flag is required — without it, Drizzle Kit adds a numeric prefix to the filename and the -o path in the next step won’t match:

    Terminal window
    npx drizzle-kit generate --custom --name=better_tenant_rls --prefix=none
  5. 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
  6. Apply the RLS migration:

    Terminal window
    npx drizzle-kit migrate
  7. Verify setup:

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

When you add a new tenant-scoped table after initial setup:

  1. Add the table to your Drizzle schema (with tenant_id column):

    import { tenantId } from "@usebetterdev/tenant/drizzle";
    export const commentsTable = pgTable("comments", {
    id: serial("id").primaryKey(),
    body: text("body").notNull(),
    ...tenantId,
    }).enableRLS();
  2. Generate and apply the schema migration:

    Terminal window
    npx drizzle-kit generate
    npx drizzle-kit migrate
  3. Create a custom migration for RLS. The --prefix=none flag is required — without it, Drizzle Kit adds a numeric prefix and the -o path in the next step won’t match:

    Terminal window
    npx drizzle-kit generate --custom --name=comments_rls --prefix=none
  4. 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
  5. Apply the RLS migration:

    Terminal window
    npx drizzle-kit migrate
  6. Add the table name to tenantTables in your config.

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";
  • 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