Skip to content

Database managers

Database managers are classes used to create, delete, and check the existence of tenant databases.

This is best explained with actual code, so here’s what the main interface looks like:

src/Database/Contracts/TenantDatabaseManager.php
interface TenantDatabaseManager
{
/** Create a database. */
public function createDatabase(TenantWithDatabase $tenant): bool;
/** Delete a database. */
public function deleteDatabase(TenantWithDatabase $tenant): bool;
/** Does a database exist? */
public function databaseExists(string $name): bool;
/** Construct a DB connection config array. */
public function makeConnectionConfig(array $baseConfig, string $databaseName): array;
}

All managers except SQLiteDatabaseManager also implement this interface (no connection is needed with SQLite since it does filesystem operations):

src/Database/Contracts/StatefulTenantDatabaseManager.php
interface StatefulTenantDatabaseManager extends TenantDatabaseManager
{
/** Get the DB connection used by the tenant database manager. */
public function connection(): Connection;
/**
* Set the DB connection that should be used by the tenant database manager.
*
* @throws NoConnectionSetException
*/
public function setConnection(string $connection): void;
}

With this default implementation:

src/Database/TenantDatabaseManagers/TenantDatabaseManager.php
abstract class TenantDatabaseManager implements StatefulTenantDatabaseManager
{
/** The database connection to the server. */
protected string $connection;
public function connection(): Connection
{
if (! isset($this->connection)) {
throw new NoConnectionSetException(static::class);
}
return DB::connection($this->connection);
}
public function setConnection(string $connection): void
{
$this->connection = $connection;
}
public function makeConnectionConfig(array $baseConfig, string $databaseName): array
{
$baseConfig['database'] = $databaseName;
return $baseConfig;
}
}

Individual database managers then implement these methods, like this:

src/Database/TenantDatabaseManagers/MySQLDatabaseManager.php
class MySQLDatabaseManager extends TenantDatabaseManager
{
public function createDatabase(TenantWithDatabase $tenant): bool
{
$database = $tenant->database()->getName();
$charset = $this->connection()->getConfig('charset');
$collation = $this->connection()->getConfig('collation');
return $this->connection()->statement("CREATE DATABASE `{$database}` CHARACTER SET `$charset` COLLATE `$collation`");
}
public function deleteDatabase(TenantWithDatabase $tenant): bool
{
return $this->connection()->statement("DROP DATABASE `{$tenant->database()->getName()}`");
}
public function databaseExists(string $name): bool
{
return (bool) $this->connection()->select("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$name'");
}
}

These managers are configured in config/tenancy.php, database.managers array:

config/tenancy.php
'managers' => [
'sqlite' => Stancl\Tenancy\Database\TenantDatabaseManagers\SQLiteDatabaseManager::class,
'mysql' => Stancl\Tenancy\Database\TenantDatabaseManagers\MySQLDatabaseManager::class,
'mariadb' => Stancl\Tenancy\Database\TenantDatabaseManagers\MySQLDatabaseManager::class,
'pgsql' => Stancl\Tenancy\Database\TenantDatabaseManagers\PostgreSQLDatabaseManager::class,
'sqlsrv' => Stancl\Tenancy\Database\TenantDatabaseManagers\MicrosoftSQLDatabaseManager::class,
// ...
],

Which manager should be used is determined by the tenant’s template connection’s driver value.

The template connection, as explained on the customizing databases page, defaults to the central connection, which again is why your tenants will be using your MySQL server (as well as the MySQL manager shown above) when your central connection is a MySQL connection. If you change your central connection to PostgreSQL, your tenants will be using your PostgreSQL server and the configured PostgreSQL database manager. And so on.

The only thing remaining to understand is how these managers are actually executed. If you take a look at your TenancyServiceProvider, you’ll see this job pipeline:

app/Providers/TenancyServiceProvider.php
Events\TenantCreated::class => [
JobPipeline::make([
Jobs\CreateDatabase::class,
Jobs\MigrateDatabase::class,
// Jobs\SeedDatabase::class,
// Jobs\CreateStorageSymlinks::class,
// Your own jobs to prepare the tenant.
// Provision API keys, create S3 buckets, anything you want!
])->send(function (Events\TenantCreated $event) {
return $event->tenant;
})->shouldBeQueued(false),
],

A job pipeline is our own abstraction for turning an ordered list of jobs into a listener, that may be optionally queued. What happens here is that, upon tenant creation, we first try to create a database. If that succeeds, we try to also migrate the database. If you uncomment any of the other jobs, they will be executed too — in order. Migrations depend on the database being created, and seeders depend on the database being migrated (having tables).

In this case, CreateDatabase calls $this->tenant->database()->manager()->createDatabase($this->tenant).

$tenant->database() returns an instance of DatabaseConfig which is where the tenancy_db_* properties are merged with the configured template connections and so on. The manager() method returns the TenantDatabaseManager instance which then executes the actual CREATE DATABASE ... statements.

Since these managers are used in event listeners (the job pipelines), you should also note that simply dropping all tenants will not delete their databases since bulk deletes do not trigger events in Laravel. Instead, the proper approach is using something like Tenant::cursor()->each->delete().

This is a common pain point in local dev environments where developers may execute php artisan migrate:fresh often. To solve that, you can set the tenancy.database.drop_tenant_databases_on_migrate_fresh config key to true, which will override the default MigrateFresh command with our override that makes sure to delete each tenant one by one, triggering event listeners.

Finally, you should also know that these job pipelines can be queued. If your tenant creation pipeline is slow (for instance if it involves a bunch of API calls that might be provisioning some resources) or error-prone (again due to a reliance on third-party services, making some retry logic desirable) you may want to enable queuing — simply set shouldBeQueued(true).

Note that this affects tenant onboarding. If you use a standard approach where tenants “registering” triggers impersonation that redirects the tenant to their domain, the tenant may not be fully created yet. You can add some error handling logic for specific types of query exceptions to render a “We’re building your app” message and automatically refresh the page for the user. See the related page in our sponsor docs for a copy-pasteable implementation of this.

As an alternative to queuing tenant creation, you can use the pending tenants feature to maintain a pool of pre-created tenants that are ready to use during tenant signup.

Many of the database managers our package ships with also have a “permission controlled” version. That is, a version which in addition to managing tenant databases also manages database users. In other words, each tenant gets a separate database user they connect to, not just a database.

The main benefit of using a permission controlled database manager is extra security: if the database connection is hijacked through something like SQL injection, the attacker will not be able to reach other databases (including the central database) with the hijacked connection, as it only has access to the tenant’s database and nothing else.

Assuming your central connection user has permissions to create users as well as databases, using a permission controlled manager is as simple as swapping the database manager in your Tenancy config file:

config/tenancy.php
'managers' => [
'mysql' => Stancl\Tenancy\Database\TenantDatabaseManagers\MySQLDatabaseManager::class,
'mysql' => Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledMySQLDatabaseManager::class
// ...
],

As demonstrated on the Customizing databases page, simply running Tenant::create() will set tenancy_db_name to the default prefix, a generated ID, and a suffix (empty by default):

php artisan tinker
> $tenant = App\Models\Tenant::create();
= App\Models\Tenant {#5941
data: null,
id: "b37609c7-035c-4a68-aff5-ac60ff334699",
updated_at: "2025-09-26 12:29:39",
created_at: "2025-09-26 12:29:39",
tenancy_db_name: "tenantb37609c7-035c-4a68-aff5-ac60ff334699",
}

The tenancy_db_name attribute needs to be set before the database is created. The package achieves this by calling $tenant->database()->makeCredentials() in CreateDatabase before running the actual createDatabase() call.

The same thing applies to the username and password of the tenant’s database user. If a permission controlled database manager is used, makeCredentials() also generates a username (using DatabaseConfig::$usernameGenerator) and a password (using DatabaseConfig::$passwordGenerator). Those get stored on the tenant as internal keys and then used when the permission controlled database manager creates/drops the respective database:

PermissionControlledMySQLDatabaseManager.php
public function createUser(DatabaseConfig $databaseConfig): bool
{
$database = $databaseConfig->getName();
$username = $databaseConfig->getUsername();
$password = $databaseConfig->getPassword();
$this->connection()->statement("CREATE USER `{$username}`@`%` IDENTIFIED BY '{$password}'");
$grants = implode(', ', static::$grants);
if ($this->isVersion8()) { // MySQL 8+
$grantQuery = "GRANT $grants ON `$database`.* TO `$username`@`%`";
} else { // MySQL 5.7
$grantQuery = "GRANT $grants ON `$database`.* TO `$username`@`%` IDENTIFIED BY '$password'";
}
return $this->connection()->statement($grantQuery);
}

The closest SQLite equivalent of accessing a “different tenant’s database” is using the ATTACH statement to access data in another database file.

Therefore, for feature parity, we ship with a feature for disabling ATTACH in SQLite: DisallowSqliteAttach.