basalt / tenancy/src / TenancyPluginOptions
Interface: TenancyPluginOptions
Defined in: tenancy/src/index.ts:572
Properties
canonicalDomain?
> optional canonicalDomain?: (tenant) => string | undefined
Defined in: tenancy/src/index.ts:673
The address a new tenant is reachable at, applied by tenancy.create() before the record is persisted.
canonicalDomain: (tenant) => `${tenant.id}.${process.env.APP_DOMAIN}`Without it an application has to remember domains at every creation path — public signup, an admin route, a seed script — and forgetting is silent: subdomainResolver answers from the Host, so the firm works and the table stays empty until somebody asks for a custom domain.
Return undefined for a tenant that should have none.
Parameters
tenant
Returns
string | undefined
onConflict?
> optional onConflict?: ResolverConflictPolicy
Defined in: tenancy/src/index.ts:582
See ResolverConflictPolicy. Default 'precedence'.
onDeprovision?
> optional onDeprovision?: (tenant) => void | Promise<void>
Defined in: tenancy/src/index.ts:657
Tears a tenant's storage down — the counterpart to onProvision, and its mirror image: DROP SCHEMA, delete the bucket prefix, drop the database.
Runs inside the tenant's context, so a tenant-scoped client points at the storage being removed. Make it idempotent (DROP SCHEMA IF EXISTS): if it throws, the record survives marked deleting and a retry has to be able to finish.
Without it, tenancy.destroy() still removes the record — and says so, so nobody discovers by accident that the schema is still there.
Parameters
tenant
Returns
void | Promise<void>
onMigrate?
> optional onMigrate?: (tenant) => void | Promise<void>
Defined in: tenancy/src/index.ts:612
Per-tenant migration hook for basalt tenant:migrate. The framework iterates tenants and runs this inside each one's context; you provide the DB-specific work (e.g. prisma migrate deploy against the tenant's schema).
Parameters
tenant
Returns
void | Promise<void>
onProvision?
> optional onProvision?: (tenant) => void | Promise<void>
Defined in: tenancy/src/index.ts:644
Brings a NEW tenant's storage into existence — create the schema or database, then migrate it. Runs inside the new tenant's context, from tenancy.create() and from basalt tenant:create, before anything can route a request to it.
This is what makes self-service signup work: a tenant created from an admin panel has no operator standing by to run basalt tenant:migrate, and without provisioning its first request hits storage that does not exist.
tenancyPlugin({
source, resolvers,
async onProvision(tenant) {
const admin = new PrismaClient()
await provisionTenantSchema(admin, tenantSchema(tenant.id))
await migrateTenants({
tenants: [tenant.id],
target: { mode: 'schema', url: process.env.DATABASE_URL!, provision: admin },
})
},
})Make it idempotent (CREATE SCHEMA IF NOT EXISTS, migrate deploy): if it throws, the tenant record already exists and a retry has to be able to finish the job. Keep it quick, or hand the slow part to a queued job — this runs inline, so an HTTP handler calling create() waits for it.
Parameters
tenant
Returns
void | Promise<void>
onSeed?
> optional onSeed?: (tenant) => void | Promise<void>
Defined in: tenancy/src/index.ts:614
Per-tenant seed hook for basalt tenant:seed, run inside each tenant's context.
Parameters
tenant
Returns
void | Promise<void>
provision?
> optional provision?: "inline" | "deferred"
Defined in: tenancy/src/index.ts:703
When onProvision runs.
'inline' (default) — create() waits for it, so the caller knows the tenant is usable when it returns. Right for a schema and a few migrations.
'deferred' — create() returns as soon as the record is written, marked provisioning; the resolver answers 503 for that tenant until someone calls tenancy.provision(id). Use it when provisioning is slow enough to outlive an HTTP request.
Deferred does NOT schedule anything by itself, and deliberately so: background work runs in another process, where a closure from this one cannot reach. You dispatch the job and it calls back in:
const ProvisionTenant = defineJob({
name: 'tenant.provision',
handle: ({ id }: { id: string }) => ctx().container.get(TENANCY).provision(id),
})
app.hooks.on('tenancy:created', …) // fires when provisioning finishes
await tenancy.create({ id }) // returns immediately
await queue.dispatch(ProvisionTenant, { id })That keeps @basaltkit/queue out of this package entirely — the app owns the dispatch, and any scheduler works.
required?
> optional required?: boolean | { except: (string | RegExp)[]; }
Defined in: tenancy/src/index.ts:606
Reject requests without a tenant (404 TENANCY_NOT_RESOLVED). Default: false.
true applies to every route, which is rarely what an app can live with: a health check has no tenant to send, and neither does a landing page or a public pricing endpoint. Pass { except } to exempt those paths — exact strings or regular expressions, matched against the path without its query string.
required: { except: ['/', '/health', /^/public//] }Exempting a path only lifts the tenant requirement. Auth, subscription and every other guard still apply.
A route can also declare this for itself, which overrides whatever is set here — see meta.tenant on the route:
route({ method: 'GET', url: '/pricing', meta: { tenant: false }, handler })resolvers
> resolvers: TenantResolver[]
Defined in: tenancy/src/index.ts:580
Authoritative resolvers (subdomain, domain, route, or authoritative(fn)) run first; a tenant they name that does not exist resolves to NO tenant rather than falling through. Fallbacks (header, unmarked custom resolvers) run, in list order, only when no authoritative resolver named anything.
source
> source: TenantSource
Defined in: tenancy/src/index.ts:573
validateTenantId?
> optional validateTenantId?: (id) => boolean
Defined in: tenancy/src/index.ts:716
The tenant-id grammar tenancy.create() (and basalt tenant:create) enforces; an id it rejects throws InvalidTenantIdError (400) before anything is written. tenancy.run() enforces it too, and a resolver ref whose id fails it resolves to no tenant. Default isValidTenantId: /^[a-z0-9][a-z0-9_-]{0,62}$/, minus the reserved id global.
Tenant ids become namespace segments (tenant:<id>: cache keys, tenants/<id>/ storage paths, schema names), so keep any replacement free of :, /, \\, ., whitespace and control characters. Pass the same function to new MemoryTenantSource({ validateTenantId }) if you use it.
Parameters
id
string
Returns
boolean