Tenant identification
Strategies
Configure via resolverStrategy in config/multitenancy.ts:
| Strategy | How it works | Best for |
|---|---|---|
header (default) | Reads x-tenant-id from request headers | Internal APIs, mobile |
subdomain | Extracts UUID from <uuid>.yourdomain.com | SaaS web apps |
path | Reads the first path segment /<uuid>/... | API versioning, embeds |
request-data | Reads from query string or body | Webhook receivers |
domain-or-subdomain | Custom domain wins, falls back to subdomain | Mixed deployments |
Header
The default. Read from the configurable tenantHeaderKey (defaults to x-tenant-id).
// 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:
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.
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.
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:
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:
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:
export default defineConfig({
resolverChain: ['header', 'subdomain', 'request-data'],
})Useful when the same app serves both human web traffic and machine APIs.
Read next
- Routing. The
tenant(),central(), anduniversal()route-group macros and custom-domain mapping. - Bootstrappers; what happens once the tenant is identified.