After conducting an extensive two-week search for a comprehensive guide on implementing multi-tenancy within my SaaS project, I regrettably found no fully documented resources. Consequently, I resorted to seeking assistance through Filament's support channels, where I received invaluable assistance from knowledgeable individuals.
I diligently applied the guidance and recommendations provided, addressing one issue after another, ultimately achieving a functional multi-tenancy setup within my project. Recognizing the challenges I encountered, I have undertaken the task of sharing my experiences and the solutions that worked for me, with the aim of simplifying the process for future developers seeking to establish a multi-tenancy SaaS model.
The primary objective of this endeavor is to facilitate the installation of a multi-tenancy SaaS architecture, wherein a single database encompasses all created domains, while each tenant enjoys their own dedicated database.
Before we begin, I would like to extend my heartfelt gratitude to Geoff for the invaluable help, unwavering support, and diligent debugging of issues. His contributions have been instrumental in the creation of this comprehensive documentation guide. Your assistance has made this gist possible, and for that, we express our sincere thanks.
you need to require stancl/tenancy
in your current project by running the following command in the terminal
composer require stancl/tenancy
then you need to run this command
php artisan tenancy:install
in case of success, you should see the following in your terminal
now you have one choice of these choises
- if you started your project with installing multi-tenancy first you should run the following command
php artisan migrate
-
if you already have migrations files, you need to consider adding your own migrations under a directory called
tenant
undermigrations
which is underdatabase
directory, just like the following screenshotthen run the command :
php artisan migrate
Register the service provider in onfig/app.php
, Make sure it's on the same position as in the code snippet below:
now, you need to create your own Tenant Model by running the php artisan command:
php artisan make:model Tenant
replace the code inside the model with the following
<?php
namespace App\Models;
use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;
use Stancl\Tenancy\Contracts\TenantWithDatabase;
use Stancl\Tenancy\Database\Concerns\HasDatabase;
use Stancl\Tenancy\Database\Concerns\HasDomains;
class Tenant extends BaseTenant implements TenantWithDatabase
{
use HasDatabase, HasDomains;
}
Now you need to tell the package to use this custom model. Open the config/tenancy.php
file and modify the line below:
'tenant_model' => \App\Models\Tenant::class,
the most important steps now, We'll make a small change to the app/Providers/RouteServiceProvider.php
file. Specifically, we'll make sure that central routes are registered on central domains only.
protected function mapWebRoutes()
{
foreach ($this->centralDomains() as $domain) {
Route::middleware('web')
->domain($domain)
->namespace($this->namespace)
->group(base_path('routes/web.php'));
}
}
protected function mapApiRoutes()
{
foreach ($this->centralDomains() as $domain) {
Route::prefix('api')
->domain($domain)
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
}
}
protected function centralDomains(): array
{
return config('tenancy.central_domains');
}
Call these methods manually from your RouteServiceProvider's boot() method, instead of the $this->routes()
calls.
$this->routes(function () {
$this->mapApiRoutes();
$this->mapWebRoutes();
});
To the most important steps (.env , tenancy.php, database.php) Now we need to actually specify the central domains. A central domain is a domain that serves your "central app" content, e.g. the landing page where tenants sign up. Open the config/tenancy.php file and add them in:
'central_domains' => [
'localhost'
],
and to setup database.php
connectios, i'm using mysql
you can replace the file with the following script and change the connection name as you like
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Database Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the database connections below you wish
| to use as your default connection for all database work. Of course
| you may use many connections at once using the Database library.
|
*/
'default' => env('DB_CONNECTION', 'mysql'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
|
| Here are each of the database connections setup for your application.
| Of course, examples of configuring each database platform that is
| supported by Laravel is shown below to make development simple.
|
|
| All database work in Laravel is done through the PHP PDO facilities
| so make sure you have the driver for your particular database of
| choice installed on your machine before you begin development.
|
*/
'connections' => [
'mysql_landlord' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('LANDLORD_DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'mysql_tenant' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
|
| This table keeps track of all the migrations that have already run for
| your application. Using this information, we can determine which of
| the migrations on disk haven't actually been run in the database.
|
*/
'migrations' => 'migrations',
];
and for the database configuration in .env file
LANDLORD_DB_DATABASE=(database-name)
DB_CONNECTION=mysql_landlord
DB_DATABASE=(database-name)
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USERNAME=(your-sql-username)
DB_PASSWORD=(your-sql-password)
very important note :make sure to set CACHE_DRIVER
to array
now in the tenant.php, Your tenant routes will look like this by default:
Route::middleware([
'web',
InitializeTenancyByDomain::class,
PreventAccessFromCentralDomains::class,
])->group(function () {
Route::get('/', function () {
return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
});
});
latter you can replace it with any view, but this is why you will detect that it is working correctly and swtiching between tenants
in the terminal run the command php artisan tinker
$ php artisan tinker
>>> $tenant1 = App\Models\Tenant::create(['id' => 'foo']);
>>> $tenant1->domains()->create(['domain' => 'foo.localhost']);
>>>
>>> $tenant2 = App\Models\Tenant::create(['id' => 'bar']);
>>> $tenant2->domains()->create(['domain' => 'bar.localhost']);
now you find that a database is created with the name foo , visit the foo.localhost
and congraturation everything is working perfectly
Notes:
- to run migrations for tenants run the command
php artisan tenants:migrate
- to freshly run migrations for tenants run the command
php artisan tenants:migrate-fresh
- to run seeds for tenants run the command
php artisan tenants:seed
- there is a possibility of encountering an error message indicating the absence of a tenant. To address this issue, it is imperative to create a tenant associated with the central domain, as this is a prerequisite for gaining access
@Hegabovic , Thanks for this tutorial. I am using Filament 3 & Laravel 11 with single database, from different domains filament login is showing and the user can login in successfully. but the issue is, here is the domain for tenant3 like 'tenant3.localhost:8000/dashboard/login' but any user can login using this URL like tenant 2 user can login in using this domain.
So how can I fix this? From the filament login UI, only domain-based users will be able to login in, other tenant users should see the unauthorised access message.