Skip to content

Tenant identification

Strategies

Configure via resolverStrategy in config/multitenancy.ts:

StrategyHow it worksBest for
header (default)Reads x-tenant-id from request headersInternal APIs, mobile
subdomainExtracts UUID from <uuid>.yourdomain.comSaaS web apps
pathReads the first path segment /<uuid>/...API versioning, embeds
request-dataReads from query string or bodyWebhook receivers
domain-or-subdomainCustom domain wins, falls back to subdomainMixed deployments

The default. Read from the configurable tenantHeaderKey (defaults to x-tenant-id).

ts
// config/multitenancy.ts
export default defineConfig({
  resolverStrategy: 'header',
  tenantHeaderKey: 'x-tenant-id',
})

Subdomain

Extracts the leading subdomain from the Host header and treats it as the tenant identifier. Set baseDomain:

ts
export default defineConfig({
  resolverStrategy: 'subdomain',
  baseDomain: env.get('APP_DOMAIN'),
})

For wildcard TLS, see Deployment.

Path

First URL segment. /abc-123/posts resolves to tenant abc-123.

ts
export default defineConfig({
  resolverStrategy: 'path',
})

Request data

Reads from query string or request body. Both default to the key tenant_id; override per source if needed.

ts
export default defineConfig({
  resolverStrategy: 'request-data',
  requestData: {
    queryKey: 'tenant_id',  // ?tenant_id=<uuid>
    bodyKey: 'tenant_id',   // { "tenant_id": "<uuid>" } in JSON / form / multipart
  },
})

Domain or subdomain

Custom domains win, falling back to subdomain. Pair with CustomDomainMiddleware for mapping acme.com → tenant UUID.

request.tenant()

The macro added to the AdonisJS request object. Memoized per request:

ts
async show({ request }: HttpContext) {
  const tenant = await request.tenant()
}

Always call this helper rather than reading the header directly. strategy can be any of the five and bypassing the helper introduces subtle bugs.

Custom resolvers

Implement the TenantResolver contract and register it in your provider:

ts
import { TenantResolver, ResolverRegistry } from '@adonisjs-lasagna/saas-tenancy/services'

class GeoIpResolver implements TenantResolver {
  resolve(ctx) {
    const country = ctx.request.header('cf-ipcountry')
    return country?.toLowerCase() ?? null
  }
}

// In your app provider
const registry = await this.app.container.make(ResolverRegistry)
registry.register('geoip', new GeoIpResolver())

Chained resolvers

Set resolverChain to try multiple strategies in order; first one to return a non-null tenant id wins:

ts
export default defineConfig({
  resolverChain: ['header', 'subdomain', 'request-data'],
})

Useful when the same app serves both human web traffic and machine APIs.

  • Routing. The tenant(), central(), and universal() route-group macros and custom-domain mapping.
  • Bootstrappers; what happens once the tenant is identified.

Was this page helpful?

Released under the MIT License.