Laravel Multitenancy

Multi-tenancy is the ability to provide your service to multiple users (tenants) from a single hosted instance of the application. This is contrasted with deploying the application separately for each user.

composer require stancl/tenancy

Then run

php artisan tenancy:install

This will create config/tenancy.php, routes/tenant.php, app/Providers/TenancyServiceProvider.php and some new migration tables

Then add

// config/app.php
App\Providers\TenancyServiceProvider::class,

If there’s already a migration update the user migration

Schema::create('users', function (Blueprint $table) {
		...
    $table->string('domain')->unique();
		...
});

Then run

php artisan migrate:fresh

Make a Tenant model

php artisan make:model Tenant
namespace App;

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;
}

Then update

// config/tenancy.php
'tenant_model' => \App\Models\Tenant::class,

Update the RouteServiceProvider

// RouteServiceProvider

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', []);
}

Then update in boot() of  RouteServiceProvider

// comment $this->routes(function () {}); add this instead
$this->routes(function () {
    $this->mapApiRoutes();
    $this->mapWebRoutes();
});

Add to .env

// localhost or domain tenant.test
APP_CENTRAL_DOMAIN=localhost 

We will create tenants after the user registers or you can use Tinker.

$user = User::create(['name' => 'madindo','email' => 'madindo@gmail.com', 'password' => bcrypt('madindo123'), 'domain' => 'madindo']);
// App\\Models\\User
public static function booted() {
    static::created(function($user) {
        $user_tenant = Tenant::create(['id' => $user->domain]);
        $user_tenant->domains()->create(['domain' => $user->domain . '.' . env('APP_CENTRAL_DOMAIN')]);
    });
}

For local development, you may use *.localhost domains (like foo.localhost) for tenants. On many operating systems, these work the same way as localhost.

Try to access {user_domain}.tenant.test / {user_domain}.localhost

This is the start of multi-tenancy, there’s a lot more to develop for our custom app.

php artisan make:migration create_settings_table --path=database/migrations/tenant
Schema::create('settings', function (Blueprint $table) {
    $table->string('key')->primary();
    $table->text('value')->nullable();
    $table->timestamps();
});
php artisan tenants:migrate

This will create a new folder in migration called tenant with migrations specifically for tenants.

Let’s make the model

php artisan make:model Tenant/Setting
namespace App\\Models\\Tenant;

use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
use Illuminate\\Database\\Eloquent\\Model;

class Setting extends Model
{
    use HasFactory;
    protected $keyType = 'string';
    protected $primaryKey = 'id';
}

[Update 14/09/2024]

Redirect when accessing the wrong subdomain

Find TenantServiceProvider.php find method boot() add this

// Handling tenant identification failure
InitializeTenancyByDomain::$onFail = function ($exception, $request, $next) {
	return redirect(env('APP_URL'));
};