Skip to content

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.

ts
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 ​

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 ​

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 ​

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.

ts
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 ​

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 ​

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:

ts
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.

ts
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:

ts
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

Released under the MIT License.