Skip to content

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:

php
$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:

php
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:

php
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:

php
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:

php
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:

php
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:

php
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:

php
class TenantAwareJob
{
    public function handle(object $job, Closure $next): void
    {
        if (method_exists($job, 'restoreTenantContext')) {
            $job->restoreTenantContext();
        }

        $next($job);
    }
}

Configuration

php
return [
    'organization_type' => 'tenant',
    'organization_model' => \App\Models\Tenant::class,
    'database_strategy' => 'multi',
    'sub_organization' => null,
    'has_sub_teams' => false,
];