basalt / tenancy/src / Tenancy
Class: Tenancy
Defined in: tenancy/src/index.ts:124
Constructors
Constructor
> new Tenancy(options): Tenancy
Defined in: tenancy/src/index.ts:138
new Tenancy({ source, resolvers, ... }) — see TenancyOptions.
Parameters
options
Returns
Tenancy
Constructor
> new Tenancy(source, resolvers, hooks?, onProvision?, provisionMode?, onDeprovision?, canonicalDomain?, validateTenantId?, resolution?): Tenancy
Defined in: tenancy/src/index.ts:143
The positional form, kept for compatibility. Prefer the options object: nine positional arguments are easy to misalign.
Parameters
source
resolvers
hooks?
onProvision?
(tenant) => void | Promise<void>
provisionMode?
"inline" | "deferred"
onDeprovision?
(tenant) => void | Promise<void>
canonicalDomain?
(tenant) => string | undefined
validateTenantId?
(id) => boolean
resolution?
onConflict?
Returns
Tenancy
Methods
create()
> create(tenant): Promise<Tenant>
Defined in: tenancy/src/index.ts:231
Registers a tenant and brings its storage into existence.
Use this rather than source.create() directly. The source only persists the record; a tenant whose row exists but whose schema does not is immediately routable by subdomainResolver/domainResolver and fails on its very first request with a raw database error. Going through here runs onProvision before anyone can reach it.
onProvision runs INSIDE the new tenant's context, like onMigrate and onSeed, so ctx().tenant and any tenant-scoped client resolve correctly. Entering the context opens no connection by itself, so provisioning work on an admin connection (CREATE SCHEMA) is still fine.
If provisioning throws, the error propagates and tenancy:created does not fire — but the tenant record has already been written, because the source persisted it first. That half-state is not rolled back: deleting is not something every TenantSource can do, and a failed delete on top of a failed provision loses the evidence. Provisioning is expected to be idempotent so that a retry finishes the job — and that retry is provision(id), not a second create().
An existing id is refused with TenantAlreadyExistsError (409), whatever its status: nothing is written, no hook fires, onProvision does not run. Overwriting was never what a caller meant. The durable sources' save is an upsert that replaces the whole record, so a double-submitted signup used to erase the tenant's owner, reactivate a suspended account and re-run provisioning over live data. Use the source's save() for an intentional update.
Parameters
tenant
Returns
Promise<Tenant>
current()
> current(): Tenant | undefined
Defined in: tenancy/src/index.ts:193
The tenant of the active context, if any.
Returns
Tenant | undefined
destroy()
> destroy(id, options?): Promise<void>
Defined in: tenancy/src/index.ts:335
Removes a tenant: its storage first, then its record.
The order is the whole design. Three steps, and each one is where it is because the alternative loses something:
- Mark
deleting. The resolver stops serving the tenant before anything is torn down. Dropping a schema out from under live requests produces errors nobody can interpret, from a tenant that looked healthy a second earlier. - Run
onDeprovisioninside the tenant's context, likeonProvision, so a tenant-scoped client resolves to the storage being removed rather than to whatever the caller happened to be in. - Delete the record last. The record is the only thing naming that storage. Delete it first and a failed teardown leaves a schema nobody can find, which is exactly the state a half-finished signup used to leave behind — the reason this method exists.
If step 2 throws, the record survives, marked deleting, and the error propagates: the evidence is kept and a retry can finish the job. force removes the record anyway — for when the storage is already gone by other means. It is a deliberate way to orphan storage, so it is never the default.
Parameters
id
string
options?
force?
boolean
Returns
Promise<void>
find()
> find(id): Promise<Tenant | null>
Defined in: tenancy/src/index.ts:197
Parameters
id
string
Returns
Promise<Tenant | null>
forEach()
> forEach(fn, options?): Promise<void>
Defined in: tenancy/src/index.ts:482
Runs fn once per tenant, with bounded concurrency — bulk maintenance.
Parameters
fn
(tenant) => void | Promise<void>
options?
concurrency?
number
Returns
Promise<void>
provision()
> provision(tenantOrId): Promise<Tenant>
Defined in: tenancy/src/index.ts:291
Runs onProvision for a tenant and flips its status to ready — or failed, then rethrows.
Public because background provisioning happens in ANOTHER PROCESS. A job handler cannot receive a closure from the process that created the tenant, so the worker re-enters here with the id and the app's own onProvision:
defineJob({
name: 'tenant.provision',
handle: ({ id }) => ctx().container.get(TENANCY).provision(id),
})Idempotent by requirement, not by construction: onProvision may run again after a failure, so write it with CREATE SCHEMA IF NOT EXISTS and migrate deploy.
Parameters
tenantOrId
string | Tenant
Returns
Promise<Tenant>
resolve()
> resolve(request): Promise<Tenant | null>
Defined in: tenancy/src/index.ts:412
Identifies the request's tenant.
Authoritative resolvers first (subdomain, domain, route — anything the platform controls), in list order; the first ref that loads a tenant wins. If any of them named a tenant and none loaded, the answer is no tenant: the request does not fall through to a client-controlled resolver, so nosuch.app.com + x-tenant-id: globex is not globex. Only when no authoritative resolver named anything are the fallbacks (header, unmarked custom resolvers) tried, in list order.
A ref whose id fails the tenant-id grammar, or whose domain is not a hostname, counts as not found — it never reaches the source.
With onConflict: 'error' every resolver runs and two refs that load DIFFERENT tenants throw TenantResolutionConflictError (400).
Parameters
request
Returns
Promise<Tenant | null>
run()
> run<T>(tenantOrId, fn): Promise<T>
Defined in: tenancy/src/index.ts:468
Runs fn inside the tenant's context (preserving the surrounding context) and emits 'tenancy:switched'.
The id — given, or on the tenant object — must pass the tenant-id grammar (InvalidTenantIdError, 400): it becomes a namespace segment in every tenant-scoped package, so a hand-built { id: '../x' } must never become the context tenant.
Type Parameters
T
T
Parameters
tenantOrId
string | Tenant
fn
() => T | Promise<T>
Returns
Promise<T>