Courses

Laravel 12 Multi-Tenancy: All You Need To Know

Multiple Tenants and Switching Between Them

Summary of this lesson:
- Adding current_tenant_id to users table
- Creating UI for tenant switching in navigation
- Implementing tenant switching functionality
- Updating FilterByTenant trait to use current tenant

Now, let's take care of the situation of multiple tenants per user.

We have the database structure already, with tenant_user pivot table. Now, the question is where to save the active current tenant.


Saving Current Tenant

Two choices that I would suggest:

  1. A boolean column is_active in the pivot.
  2. Saving current tenant ID in the users table.

In this lesson, we will use the second method and add the current_tenant_id column to the users table.

First, the migration.

php artisan make:migration "add current tenant id to users table"

database/migrations/xxx_add_current_tenant_id_to_users_table.php:

Schema::table('users', function (Blueprint $table) {
$table->foreignId('current_tenant_id')->nullable()->constrained('tenants');
});

app/Models/User.php:

class User extends Authenticatable
{
protected $fillable = [
'name',
'email',
'password',
'current_tenant_id',
];
 
// ...
}

Let's refresh the database so we can make sure that everything works.

php artisan migrate:fresh

Then, we need to set the active tenant when a user registers.

app/Http/Controllers/Auth/RegisteredUserController.php:

class RegisteredUserController extends Controller
{
// ...
 
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
 
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
 
$tenant = Tenant::create(['name' => $request->name . ' Team']);
$tenant->users()->attach($user);
$user->update(['current_tenant_id' => $tenant]);
 
event(new Registered($user));
 
Auth::login($user);
 
return redirect(route('dashboard', absolute: false));
}
}

After registering in the database, we see that the current_tenant_id is set correctly.

Now, let's register with a new user and manually add the second user to the first tenant.


Switching Between Tenants

Next, let's add the ability to switch between tenants. We will add the switch above the logout button in the navigation.

resourves/views/layouts/navigation.blade.php:

// ...
 
<x-slot name="content">
<x-dropdown-link :href="route('profile.edit')">
{{ __('Profile') }}
</x-dropdown-link>
 
@if (auth()->user()->tenants()->count() > 1)
@foreach (auth()->user()->tenants as $tenant)
<x-dropdown-link :href="route('tenants.change', $tenant->id)" @class(['font-bold' => auth()->user()->current_tenant_id == $tenant->id])>
{{ $tenant->name }}
</x-dropdown-link>
@endforeach
@endif
 
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
 
<x-dropdown-link :href="route('logout')"
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-dropdown-link>
</form>
</x-slot>
 
// ...

Next, let's create a Route with the name tenants.change and a Controller for it.

php artisan make:controller TenantController --invokable

route/web.php:

Route::get('/', function () {
return view('welcome');
});
 
Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
 
Route::middleware('auth')->group(function () {
Route::get('tenants/change/{tenantId}', \App\Http\Controllers\TenantController::class)->name('tenants.change');
 
Route::resource('tasks', \App\Http\Controllers\TaskController::class);
Route::resource('projects', \App\Http\Controllers\ProjectController::class);
 
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});
 
require __DIR__.'/auth.php';

In the Controller, we must do three things:

  1. Find the tenant in the database
  2. Change the active tenant for the user
  3. Redirect to the home page

app/Http/Controllers/TenantController.php:

class TenantController extends Controller
{
public function __invoke($tenantId)
{
// Check tenant
$tenant = auth()->user()->tenants()->findOrFail($tenantId);
 
// Change tenant
auth()->user()->update(['current_tenant_id' => $tenant->id]);
 
// Redirect to dashboard
return redirect()->route('dashboard');
}
}

In the navigation dropdown, we can see a list of tenants the user has:

The tenant in bold is active. After selecting another tenant, it would seem nothing happened, but the tenant should be changed.

Lastly, we must change the FilterByTenant trait to how we get the current tenant ID.

app/Traits/FilterByTenant.php:

trait FilterByTenant
{
protected static function booted(): void
{
$currentTenantId = auth()->user()->tenants()->first()->id;
$currentTenantId = auth()->user()->current_tenant_id;
 
static::creating(function (Model $model) use ($currentTenantId) {
$model->tenant_id = $currentTenantId;
});
 
static::addGlobalScope(function (Builder $builder) use ($currentTenantId) {
$builder->where('tenant_id', $currentTenantId);
});
}
}

If you create a project or task in one tenant and switch to another, you shouldn't see those records.


To complete our multi-tenancy system, we must take care of two things:

  • Identifying the tenant by subdomain
  • And inviting users to the tenant

Follow the next lessons for those topics.

Previous: Filter DB Records by Active Tenant
avatar

hi, i am new to laravel and vue3 and have been following your program, so foar its been helpfull.

i a building a app with seperate front end and backend, so my app is essentailly based on apis, now i am stck i need to understand how do i add global scopes created in laravel into vue 3 front end

any help would be much appreciated

regards rahul

avatar

further my use case here is to crete workspace and all the back end resoureces to be restricted by the workspace,

need 1: alll user are in default workspace and only few have ability to create and work on a new workspace. need 2:all files and foleder in a work space are restricted to the users with ability to view/moderate the workspace

i have followed ur instructions and have a api to manager/moderate workspace but i need all models to be restricted by the workspace, only issue is reactivity at the fronend with vue

in this example you have scope defiend on the blade with works like a charm but i have not been able to replicate in vue front end which is completely separate.

Please help

avatar

Well, in theory it should work like this:

From your separate front-end, you ask the API with the user credentials, and then by that user you get the data only for that user.

So it doesn't change on the back-end, it's still the same global scope. The problem is probably how you authenticate user from the front-end.

avatar
You can use Markdown
avatar

Hi, Constrained allows table name and field, so that relationship can be make also like this $table->foreignId('current_tenant_id')->nullable()->constrained('tenants','id');

👍 3
🥳 1
avatar

thank you for this

avatar

is this is true: $user->update(['current_tenant_id' => $tenant]); regards

avatar

'current_tenant_id' => $tenant->id

avatar
You can use Markdown
avatar

Hi there, great job on the tutorial!

I used to approach multitenancy by storing the current tenant in a session key.

Whenever the user switched tenants, I would rewrite the session key. Is there a problem with that aproach? Maybe security issues or something else?

avatar

As long as you are checking with a middleware that the session value (tenant id or identifier) actually belongs to the user - you should be good to go

avatar

Great! thanks

avatar
You can use Markdown
avatar
You can use Markdown