Skip to content

Instantly share code, notes, and snippets.

@Hegabovic
Created October 27, 2023 21:33
Show Gist options
  • Save Hegabovic/81d63f16dcceac16ecbdd0c3722857ae to your computer and use it in GitHub Desktop.
Save Hegabovic/81d63f16dcceac16ecbdd0c3722857ae to your computer and use it in GitHub Desktop.
Full Guide for Multi-tenancy with tenancy with laravel package

TenancyWithLaravel

Full Guide Multi-Tenancy Implementation

Multi-Tenancy Implementation With Multi-Database Approach

Introduction

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.

Installation

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 Screenshot from 2023-10-27 22-53-17

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 under migrations which is under database directory, just like the following screenshot Screenshot from 2023-10-27 23-02-16

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

Screenshot from 2023-10-27 23-04-39

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

Creating 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

Author

Hegabovic

Contributors

Geoff

@Kimerrio
Copy link

Did you get this to connect to a tenant database with filament resources?

@Hegabovic
Copy link
Author

@Mahdi-Sahib no each tenant will have his/her own database
@Kimerrio yes and worked fine with me

@rrrazib
Copy link

rrrazib commented Oct 4, 2024

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment