Multi-Database
Introduction
When selecting Multi-Database as the database strategy during nubos:init, each tenant gets its own dedicated database. The tenant model stores connection credentials, and the framework switches database connections at runtime.
Additional Model Fields
The multi-database strategy adds five columns to the tenants table:
$table->string('db_host')->default('127.0.0.1');
$table->integer('db_port')->default(5432);
$table->string('db_database');
$table->string('db_username');
$table->text('db_password');The db_password field is encrypted at rest using Laravel's encrypted cast:
protected function casts(): array
{
return [
'db_password' => 'encrypted',
];
}The password is also hidden from serialization via the $hidden property.
HasTenantDatabase Trait
The HasTenantDatabase trait is added to the Tenant model and provides a method to configure and activate the tenant's database connection:
trait HasTenantDatabase
{
public function configureDatabaseConnection(): void
{
config([
'database.connections.tenant' => [
'driver' => 'pgsql',
'host' => $this->db_host,
'port' => $this->db_port,
'database' => $this->db_database,
'username' => $this->db_username,
'password' => $this->db_password,
'charset' => 'utf8',
'prefix' => '',
'schema' => 'public',
'sslmode' => 'prefer',
],
]);
DB::purge('tenant');
DB::reconnect('tenant');
}
public function getTenantConnectionName(): string
{
return 'tenant';
}
}The method writes the tenant's credentials to a tenant database connection at runtime, purges any existing connection, and reconnects.
Automatic Connection Switching
The TenantIdentification middleware automatically calls configureDatabaseConnection() when the database strategy is multi:
if (config('nubos.database_strategy') === 'multi') {
$tenant->configureDatabaseConnection();
}This happens after the tenant is identified from the subdomain, so all subsequent database queries within the request use the tenant's dedicated database.
ConfigureTenantDatabaseAction
When a new tenant is created, the ConfigureTenantDatabaseAction sets up the tenant's database connection and runs migrations:
class ConfigureTenantDatabaseAction
{
public function execute(Tenant $tenant): void
{
$tenant->configureDatabaseConnection();
Artisan::call('migrate', [
'--database' => 'tenant',
'--path' => 'database/migrations/tenant',
'--force' => true,
]);
}
}The CreateTenantAction calls this action automatically when the database strategy is multi:
if (config('nubos.database_strategy') === 'multi') {
$this->configureTenantDatabase->execute($tenant);
}Queue Jobs
Queued jobs need to restore the tenant's database connection before executing. The TenantAware trait captures the current tenant ID when the job is dispatched and restores the connection when the job runs:
trait TenantAware
{
public string $tenantId;
public function initializeTenantAware(): void
{
if (app()->bound('current_tenant')) {
$this->tenantId = app('current_tenant')->id;
}
}
public function restoreTenantContext(): void
{
$tenant = Tenant::query()->findOrFail($this->tenantId);
$tenant->configureDatabaseConnection();
app()->instance('current_tenant', $tenant);
}
}The TenantAwareJob queue middleware calls restoreTenantContext() before the job executes:
class TenantAwareJob
{
public function handle(object $job, Closure $next): void
{
if (method_exists($job, 'restoreTenantContext')) {
$job->restoreTenantContext();
}
$next($job);
}
}Configuration
return [
'organization_type' => 'tenant',
'organization_model' => \App\Models\Tenant::class,
'database_strategy' => 'multi',
'sub_organization' => null,
'has_sub_teams' => false,
];