Skip to content

Configuration reference

All configuration lives in config/multitenancy.ts, wrapped in defineConfig() so TypeScript checks the shape at build time. The stub the installer copies is a good starting point; this page is the exhaustive reference.

ts
import { defineConfig } from '@adonisjs-lasagna/saas-tenancy'

export default defineConfig({
  // …see the sections below
})

TIP

getConfig() throws until MultitenancyProvider has booted. That's the intended guard. Read config at request or job time, not at module top-level.

Core

KeyTypeDefaultMeaning
backofficeSchemaNamestringPG schema holding shared/satellite data.
backofficeConnectionNamestringLucid connection used for the backoffice schema.
centralSchemaNamestringSchema for central/global (non-tenant) tables.
centralConnectionNamestringLucid connection for the central schema.
tenantConnectionNamePrefixstringPrefix for per-tenant Lucid connection names (<prefix><tenantId>).
tenantSchemaPrefixstringPrefix for per-tenant schema names (<prefix><tenantId>).
schemaCacheTtlnumberTTL (seconds) for cached schema-existence probes.
ignorePathsstring[]Request paths that skip tenant resolution (health checks, the Stripe webhook, and so on).

Tenant resolution

KeyTypeDefaultMeaning
resolverStrategy'subdomain' | 'header' | 'path' | 'domain-or-subdomain' | 'request-data'How the tenant id is read from the request.
resolverChainstring[]Ordered resolver names; first hit wins. Overrides resolverStrategy.
tenantHeaderKeystringHeader name read by the header resolver.
baseDomainstringApex domain used to parse subdomains.
requestData.queryKeystring'tenant_id'Query-string key for the request-data resolver.
requestData.bodyKeystring'tenant_id'Body key for the request-data resolver.

Always resolve via the helper

Never read the tenant header directly. Call resolveTenantId(request), which honours resolverStrategy and resolverChain so a strategy change doesn't silently bypass your code. See Troubleshooting.

Isolation

ts
isolation: { driver: 'schema-pg' }
KeyTypeDefaultMeaning
isolation.driver'schema-pg' | 'database-pg' | 'rowscope-pg' | 'sqlite-memory''schema-pg'Isolation strategy.
isolation.templateConnectionNamestring'tenant'Connection whose config is cloned per tenant (schema-pg/database-pg).
isolation.tenantDatabasePrefixstring'tenant_'Per-tenant database name prefix (database-pg).
isolation.rowScopeTablesstring[]Tenant-scoped tables (rowscope-pg) for destroy/reset.
isolation.rowScopeColumnstring'tenant_id'Tenant id column (rowscope-pg).
isolation.rowScopeMode'strict' | 'allowGlobal''strict'strict throws on an unscoped query outside tenancy.run(). This is the safe default.

Resilience (degradation policy)

Decides, per backing dependency, whether an outage fails open (skip the check, stay available) or closed (return 503). Consumed by ResilienceService; emits a DependencyDegraded event on every degradation.

ts
resilience: {
  redis: { quota: 'fail-open', rateLimit: 'fail-closed' },
  observe: true,
}
KeyTypeDefaultMeaning
resilience.defaultPolicy'fail-open' | 'fail-closed''fail-closed'Fallback policy for anything not overridden.
resilience.redis.quota'fail-open' | 'fail-closed''fail-open'QuotaService.consume/track on a Redis outage. Fail-open returns 0 (no enforcement); fail-closed throws DependencyUnavailableException.
resilience.redis.rateLimit'fail-open' | 'fail-closed''fail-closed'RateLimitMiddleware (the per-route failOpen option still wins where set).
resilience.redis.cache'fail-open' | 'fail-closed''fail-open'Cache bootstrapper.
resilience.redis.metrics'fail-open' | 'fail-closed''fail-open'MetricsService counters.
resilience.observebooleantrueEmit DependencyDegraded + log + OTel span event on degradation.

Fail-open is silent enforcement loss

fail-open for quotas means a Redis outage stops enforcing limits. That's the right default for availability, but subscribe to DependencyDegraded so you know it's happening. Choose fail-closed where correctness beats uptime.

Circuit breaker

KeyTypeMeaning
circuitBreaker.thresholdnumberError-percentage threshold to open.
circuitBreaker.resetTimeoutnumberms in OPEN before probing (HALF_OPEN).
circuitBreaker.rollingCountTimeoutnumberms window for the rolling error stats.
circuitBreaker.volumeThresholdnumberMinimum requests in the window before the breaker can trip.

Open/closed state is persisted to Redis and restored on restart so a known-down tenant DB isn't hammered with timeouts after a deploy.

Queue, cache, backup

KeyTypeDefaultMeaning
queue.tenantQueuePrefixstringBullMQ queue-name prefix per tenant.
queue.defaultConcurrencynumberDefault worker concurrency.
queue.attemptsnumberDefault job retry attempts.
queue.redis{ host, port, username?, password?, db? }Dedicated Redis for queues (separate DB from cache.redis).
cache.ttlnumberDefault cache TTL (seconds).
cache.redis{ host, port, username?, password?, db? }Dedicated Redis for the cache.
backup.storagePathstringLocal dir for .dump archives + backup.json sidecar.
backup.metadataTtlnumberTTL (seconds) for backup metadata in Redis.
backup.pgConnection{ host, port, user, password, database }Connection used by pg_dump/pg_restore/psql.
backup.s3{ enabled, bucket, region, endpoint?, accessKeyId, secretAccessKey }Optional S3 offload (peer dep @aws-sdk/client-s3).
backup.retentionBackupRetentionConfigTiered retention (tiers, defaultTier, getTier).

Plans & billing

KeyTypeDefaultMeaning
plans.defaultPlanstringPlan applied when nothing else resolves.
plans.definitionsRecord<string, { limits: Record<string, number> }>Named plans and their quota limits.
plans.getPlan(tenant) => string | undefinedHost callback to resolve a tenant's plan.
plans.storage'config-only' | 'tenant_plans' | 'auto''auto'Where the tenant→plan assignment lives.
plans.emitTrackedbooleanfalseEmit QuotaTracked on every track/consume (enables the Stripe metering bridge).
billingBillingConfigStripe satellite. See the Billing page for the full block.

Impersonation, maintenance, soft delete

KeyTypeDefaultMeaning
impersonation.secretstringHMAC secret (≥ 32 chars). Without it, start() throws.
impersonation.defaultDurationnumber3600Session length (seconds, min 60).
impersonation.maxDurationnumber86400Hard upper bound (seconds).
impersonation.headerNamestringx-impersonation-tokenHeader read by the middleware.
impersonation.cookieNamestring__impersonationCookie fallback name.
maintenance.defaultMessagestringDefault body for TenantMaintenanceException.
maintenance.retryAfterSecondsnumber600Retry-After on the 503.
maintenance.bypassToken / bypassHeaderstringx-tenant-bypass-maintenanceShared-secret bypass. Rotate often.
softDelete.retentionDaysnumber30Days a soft-deleted tenant's schema survives before tenant:purge-expired drops it.

Doctor thresholds

doctor overrides the built-in tenant:doctor check thresholds (all optional): queueStalledMinutes (10), replicaLagWarnSeconds (30), replicaLagErrorSeconds (120), longQueryWarnSeconds (30), longQueryErrorSeconds (120), poolSaturationWarnRatio (0.9).

Read replicas

ts
tenantReadReplicas: { hosts: [{ host: 'replica-1' }], strategy: 'sticky' }
KeyTypeDefaultMeaning
tenantReadReplicas.hosts{ host, port?, user?, password?, name? }[]Pool of read replicas.
tenantReadReplicas.strategy'round-robin' | 'random' | 'sticky''round-robin'How a replica is chosen per request.
tenantReadReplicas.connectionSuffixstring'_read'Suffix for the registered replica connection name.

No automatic lag failover

Replica selection does not check lag or health. Reads can be stale, and a down replica isn't auto-skipped. Route latency-sensitive reads to the primary, or add your own health gate. See Troubleshooting.

Was this page helpful?

Released under the MIT License.