npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@usebetterdev/tenant

v0.2.1-beta.1

Published

Multi-tenancy for Postgres in minutes. RLS-based tenant isolation, framework adapters (Hono, Express, Next.js), Drizzle and Prisma ORM support.

Downloads

235

Readme

@usebetterdev/tenant

Multi-tenancy for Postgres in minutes. Database-enforced tenant isolation via Row-Level Security, with adapters for Hono, Express, and Next.js.

  • RLS-based isolation — tenant boundaries enforced at the database level, not application code
  • Framework adapters — Hono, Express, Next.js App Router
  • Drizzle ORM — first-class adapter with transaction-scoped tenant context
  • CLI — generate migrations, verify setup, seed tenants
  • Zero WHERE clauses — queries are automatically scoped to the current tenant

Install

npm install @usebetterdev/tenant
# CLI (dev dependency)
npm install -D @usebetterdev/tenant-cli

Quick start

1. Initialize config

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

This detects your tables and creates better-tenant.config.json interactively. Or create it manually:

{
  "tenantTables": ["projects", "tasks"]
}

2. Generate and apply migration

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

This creates the tenants table, RLS policies, and triggers for each table listed in tenantTables.

3. Verify setup

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

4. Seed a tenant

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

5. Wire up your app

With pg:

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 database = drizzle(pool);

export const tenant = betterTenant({
  database: drizzleDatabase(database),
  tenantResolver: { header: "x-tenant-id" },
});

With postgres.js:

import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import { betterTenant } from "@usebetterdev/tenant";
import { drizzleDatabase } from "@usebetterdev/tenant/drizzle";

const client = postgres(process.env.DATABASE_URL);
const database = drizzle(client);

export const tenant = betterTenant({
  database: drizzleDatabase(database),
  tenantResolver: { header: "x-tenant-id" },
});

6. Add framework middleware

Hono

import { createHonoMiddleware } from "@usebetterdev/tenant/hono";

app.use("*", createHonoMiddleware(tenant));

Express

import { createExpressMiddleware } from "@usebetterdev/tenant/express";

app.use(createExpressMiddleware(tenant));

Next.js App Router

import { withTenant } from "@usebetterdev/tenant/next";

export const GET = withTenant(tenant, async (request) => {
  // tenant context is available here
  return Response.json({ ok: true });
});

7. Use in handlers

// Current tenant
const ctx = tenant.getContext();
ctx.tenantId; // "550e8400-..."
ctx.tenant; // { id, name, slug, createdAt }

// Tenant-scoped database (all queries filtered by RLS)
const db = tenant.getDatabase();
const projects = await db.select().from(projectsTable);
// ^ returns only current tenant's projects — no WHERE needed

8. Shared (non-tenant) tables

Tables without RLS (lookup data, settings, feature flags) work through the same getDatabase() handle — no separate connection needed. Wrap it for convenience:

// src/database.ts
import { tenant } from "./tenant";

export function getDatabase() {
  const database = tenant.getDatabase();
  if (!database) {
    throw new Error("No active tenant context");
  }
  return database;
}

Then use getDatabase() for everything:

import { getDatabase } from "../database";

const projects = await getDatabase().select().from(projectsTable); // RLS-filtered
const categories = await getDatabase().select().from(categoriesTable); // no RLS, all rows

Subpath exports

| Import | Description | | ------------------------------ | -------------------------------------------------------------------- | | @usebetterdev/tenant | Core API: betterTenant, getContext, runAs, runAsSystem | | @usebetterdev/tenant/drizzle | Drizzle: drizzleDatabase, tenantsTable | | @usebetterdev/tenant/hono | Hono middleware: createHonoMiddleware | | @usebetterdev/tenant/express | Express middleware: createExpressMiddleware | | @usebetterdev/tenant/next | Next.js wrapper: withTenant |

Tenant resolver

Configure how the tenant ID is extracted from incoming requests. Resolution order: header > path > subdomain > JWT > custom.

tenantResolver: {
  header: "x-tenant-id",           // from request header
  path: "/t/:tenantId/*",          // from URL path segment
  subdomain: true,                 // from subdomain (acme.app.com)
  jwt: { claim: "tenant_id" },     // from JWT payload
  custom: (req) => extractTenant(req), // custom function
}

Resolving by slug

Slugs are automatically resolved to UUIDs. If a resolver returns "acme" (not a UUID), the library looks it up in the tenants table and uses the matching UUID for RLS. No extra config needed — subdomain resolution just works:

const tenant = betterTenant({
  database: drizzleDatabase(db),
  tenantResolver: { subdomain: true },
});
// acme.app.com → resolves "acme" → looks up slug → uses UUID for RLS

For custom mappings (e.g. custom domains), use resolveToId:

tenantResolver: {
  custom: (req) => req.host, // "client.com"
  resolveToId: async (domain) => {
    const mapping = await lookupDomain(domain);
    return mapping.tenantId; // UUID
  },
}

Tenant API

CRUD operations on the tenants table are available via tenant.api. All API calls run with RLS bypass (runAsSystem) so they can read and write tenants across the system. Restrict these endpoints to admins — do not expose them to regular tenant users.

| Method | Description | | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | createTenant(data) | Create a tenant. data: { name: string, slug: string } (both required, trimmed). Returns the created Tenant (id, name, slug, createdAt). | | listTenants(options?) | List tenants with pagination. options: { limit?: number, offset?: number }. Default limit 50. Returns Tenant[]. | | updateTenant(tenantId, data) | Update a tenant by ID. data: { name?: string, slug?: string }. Returns the updated Tenant. | | deleteTenant(tenantId) | Delete a tenant by ID. Returns void. |

Example:

// Create
const created = await tenant.api.createTenant({
  name: "Acme Corp",
  slug: "acme",
});

// List (e.g. admin dashboard)
const tenants = await tenant.api.listTenants({ limit: 20, offset: 0 });

// Update
await tenant.api.updateTenant(created.id, {
  name: "Acme Inc",
  slug: "acme-inc",
});

// Delete
await tenant.api.deleteTenant(created.id);

Admin operations

// Run as a specific tenant (cron jobs, background tasks)
await tenant.runAs(tenantId, async (db) => {
  await db.select().from(projectsTable); // scoped to tenant
});

// Run with RLS bypass (admin, cross-tenant reporting)
await tenant.runAsSystem(async (db) => {
  await db.select().from(projectsTable); // all tenants
});

How it works

  1. Request arrives with tenant identifier (header, path, subdomain, JWT, or custom)
  2. Middleware resolves the tenant ID and starts a database transaction
  3. SET LOCAL app.current_tenant = '<uuid>' scopes all queries via RLS
  4. Your handler runs — every query is automatically filtered to the current tenant
  5. Transaction commits, SET LOCAL auto-clears (safe for connection pooling)

Telemetry

Anonymous telemetry is on by default. Opt out with BETTER_TENANT_TELEMETRY=0 or telemetry: { enabled: false } in config.

License

MIT