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:
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):
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:
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:
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:
'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:
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.
Permission controlled database managers
Section titled “Permission controlled database managers”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:
'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):
> $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:
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);}Permission controlled SQLite
Section titled “Permission controlled SQLite”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.
In-memory SQLite databases
Section titled “In-memory SQLite databases”In-memory SQLite databases can be useful in tests, but a plain :memory: database isn’t usable with the package. The
tenant connection gets created and destroyed constantly — during migrations, and whenever tenancy is ended (we close the
tenant connection to prevent accidental references to it in the central context) — and each time the last connection to
a :memory: database closes, its contents are gone.
For this reason, when a tenant is created with :memory: as its database name, SQLiteDatabaseManager stores a named
in-memory database on the tenant instead:
file:_tenancy_inmemory_{tenant key}?mode=memory&cache=sharedNamed in-memory databases are shared between connections, so the data survives the tenant connection being closed and
reopened. They do not have process lifetime, though — SQLite frees the memory as soon as the last connection to
them is closed. Keeping that one connection open is up to you. Common approaches are storing it somewhere in the service
container, or holding a reference to it in a closure passed to register_shutdown_function().
Since there’s no file to delete, deleting a tenant with an in-memory database does nothing by default. If you need to
close your connection at that point, use the $closeInMemoryConnectionUsing static property:
SQLiteDatabaseManager::$closeInMemoryConnectionUsing = function (Tenant $tenant) { // Close/forget the connection you're keeping open for this tenant};