
Tenant directory
DNS-label slugs, memberships with three roles, archive-never-delete. The registry the rest of your app resolves against.
A tenant directory with DNS-label slugs and three roles, request→tenant resolution split into untrusted extraction and membership-checked authorization, and row-level-security isolation the scoped executor enforces per transaction — the tenantId billing-kit assumes, made true.


DNS-label slugs, memberships with three roles, archive-never-delete. The registry the rest of your app resolves against.

Untrusted extraction and membership-checked authorization are separate steps, so “trust the subdomain” is not expressible by accident.

tenancy.protect() forces row-level security per table; the scoped executor sets the tenant for the transaction and nothing leaks across.

The same seam billing-kit consumes: one opaque id, one connection pool, no second source of truth for who a tenant is.

Offboarding archives; it never destroys history. The audit trail of who belonged when stays intact.

SSO over any OIDC IdP, SCIM 2.0 provisioning, and RBAC-engine bridges — over the same seams, opt-in.
Owner, admin, member — the whole authorization surface, on purpose.
Read the tenant hint from the request — subdomain, header, path — as untrusted input.
Check the caller’s membership against the directory. Only a real member of a real tenant passes.
Run the work inside a transaction the scoped executor has bound to that tenant, with RLS enforcing it.
import { resolve, protect } from 'tenant-kit';
// untrusted extraction, then a membership-checked authorization
const tenant = await resolve(sql, { host: req.headers.host, user });
// RLS forced per table; the scoped executor binds the tenant per transaction
await protect(sql, tenant, async (db) => {
return db.query('select * from invoices'); // only this tenant's rows
});Isolation you can turn off by forgetting a WHERE clause is not isolation. Hold the boundary at the database.