Laravel Jetstream is the latest in a long line of fantastic first-party open source packages from Taylor and the (growing) Laravel team. Jetstream includes an option to add team functionality to your app, (really useful for SaaS apps!). This article is going to walk you through how to adapt a Jetstream enabled Laravel app to use multii-tenant databases.
The teams functionality has been extracted and improved from the premium product Laravel Spark.
...
We need two database connections: one for the 'landlord' database and for the 'tenant'.
mysql
connection and rename this to tenant
.mysql
connection to landlord
landlord
connection:'database' => env('DB_LANDLORD_DATABASE', 'myapp_landlord'),
DB_DATABASE
environment variable like so:DB_LANDLORD_DATABASE=getpaid2_landlord
DB_TENANT_DATABASE_PREFIX=getpaid2_
DB_CONNECTION=landlord
Publish the landlord migration from the Spatie package.
php artisan vendor:publish --provider="Spatie\Multitenancy\MultitenancyServiceProvider" --tag="migrations"
This migration will be published to database/migrations/landlord
. From now one, the database/migrations
directory will be used exclusively for the tenant migrations. Assuming you have a clean Laravel 8+ Jetstream installation, move all the migrations to the new landlord directory. If you are working on an existing application you will need to decide which migrations should be moved to the landlord directory. As a general rule anything related to the framework should be in the landlord directory e.g. teams, users, failed jobs, personal acceess tokens. Application data for a tenant should not be in the landlord schema.
Before we migrate the database we need to make a couple of modifications:
$table->foreignId('team_id')->constrained()->cascadeOnDelete();
php artisan migrate --path=database/migrations/landlord --database=landlord
First we need to publish the base configuration file.
php artisan vendor:publish --provider="Spatie\Multitenancy\MultitenancyServiceProvider" --tag="config"
Specify the connections names to use:
'landlord_database_connection_name' => 'landlord',
'tenant_database_connection_name' => 'tenant',
We need to use the provided SwitchTenantDatabaseTask
:
'switch_tenant_tasks' => [
\Spatie\Multitenancy\Tasks\SwitchTenantDatabaseTask::class,
],
We need to add a relationship to the tenant model from the Team model. Whilst we are here we can the trait UsesLandlordConnection
. The User
model should have this trait as well.
// ...
use Spatie\Multitenancy\Models\Concerns\UsesLandlordConnection;
use Spatie\Multitenancy\Models\Tenant;
class Team extends JetstreamTeam
{
use UsesLandlordConnection;
// ...
public function tenant(): HasOne
{
return $this->hasOne(Tenant::class, 'team_id');
}
}
Add the UsesTenantConnection
to the models which should live in the in the tenant databases.
The Spatie package is deliberately unopionated and therefore doens't provide a mechanism for creating the tenant databases. As we are using Jetstream to provide our appication with team functionality we need to create a tenant database when a team is created. Fortunately, Jetstream provides a perfect event we can listen for.
I highly recommend you use a queued listener for this. If something goes wrong creating the tenant database it will be easier to retry.
php artisan make:listener --queued --event="\\Laravel\\Jetstream\\Events\\TeamCreated" CreateTenantForTeam
Use this snippet for the handle method of the listener:
public function handle(TeamCreated $event)
{
$tenant = Tenant::query()->firstOrCreate([
'name' => $event->team->name,
'team_id' => $event->team->id,
]);
$databaseName = sprintf('tenant_%05d', $tenant->id);
// create if database does not exist
DB::unprepared("CREATE DATABASE IF NOT EXISTS $databaseName;");
Artisan::call("tenants:artisan --tenant={$tenant->id} -- \"migrate --database=tenant\"");
$tenant->update([
'database' => $databaseName
]);
}
Register the listener in your EventServiceProvider
:
protected $listen = [
// ...
\Laravel\Jetstream\Events\TeamCreated::class => [
CreateTenantForTeam::class
]
];
The Spatie provides a mechanism for automatically determining the current tenant but this approach won't work with Jetstream as the code is executed during booting of the application. A sensible approach is to use middleware that runs after the session has been started.
/** insert middleware code here **/
Now register this middleware:
protected $routeMiddleware = [
// ...
'tenant' => \App\Http\Middleware\DetermineCurrentTenant::class,
];
Finally, use the new middleware for all authenticated routes. A route group