Custom isolation driver
The four shipped drivers; schema-pg, database-pg, rowscope-pg, sqlite-memory; cover the common cases. If your storage shape doesn't fit (per-tenant DynamoDB tables, a SaaS metadata service, an external sharder), implement your own driver and register it.
The contract
import { IsolationDriver, TenantModelContract } from '@adonisjs-lasagna/saas-tenancy/services'
export class MyDriver implements IsolationDriver {
readonly name = 'my-driver'
async provision(tenant: TenantModelContract): Promise<void> {
// Create the tenant's storage. Idempotent — the package may retry.
}
async destroy(tenant: TenantModelContract): Promise<void> {
// Tear it down cleanly. Terminate active sessions if any.
}
async reset(tenant: TenantModelContract): Promise<void> {
// Drop and recreate. Used by `tenant:migrate:fresh`.
await this.destroy(tenant)
await this.provision(tenant)
}
async connect(tenant: TenantModelContract): Promise<void> {
// Open or register the runtime connection used by Lucid.
}
async disconnect(tenant: TenantModelContract): Promise<void> {
// Close it.
}
connectionName(tenantId: string): string {
// Synchronous resolver for the active query's connection.
return `my-driver:${tenantId}`
}
async migrate(tenant: TenantModelContract, opts: { dryRun?: boolean }): Promise<void> {
// Run migrations against this tenant's storage.
// Return without throwing on `dryRun`.
}
}Registering it
Plug the driver into the registry from your provider:
// providers/app_provider.ts
import { IsolationDriverRegistry } from '@adonisjs-lasagna/saas-tenancy/services'
import { MyDriver } from '#drivers/my_driver'
export default class AppProvider {
async boot() {
const registry = await this.app.container.make(IsolationDriverRegistry)
registry.register('my-driver', new MyDriver())
}
}Then point the config at it:
// config/multitenancy.ts
export default defineConfig({
isolation: {
driver: 'my-driver',
},
})Validate the tenant id
Always call assertSafeIdentifier(tenant.id) before interpolating the id into anything that could escape; DDL, file paths, command arguments, headers. The shipped drivers do this at every entry. The helper enforces [a-zA-Z0-9_-]{1,63}; UUID v4 always passes.
import { assertSafeIdentifier } from '@adonisjs-lasagna/saas-tenancy/services'
async provision(tenant) {
assertSafeIdentifier(tenant.id)
// …
}Idempotency contract
provisioncan be called more than once. Treat the second call as a no-op when the storage exists.destroyruns afterdisconnect. Don't assume the connection is still open.resetisdestroy+provision. The default implementation is almost always right.
Tests to write
- A test against a fake tenant id that should fail
assertSafeIdentifier; ensure your driver throws. - A test for the connection-name format; synchronous, deterministic, no side effects.
- An integration test that round-trips a tenant through
provision→connect→ run a query →disconnect→destroy.
Don't over-fit
If your driver's behaviour is "schema-pg, but with a different naming convention" or "schema-pg, but with extra GRANT calls", you almost always want to compose. Wrap SchemaPgDriver in your driver and delegate, overriding only what you need. Forking the whole thing means keeping up with bug fixes that we ship; composition keeps you on the upgrade path.