Add Multi-Tenancy to Laravel Filament: Simple Way

Multi-tenancy is pretty common in web projects - when you want to give records access only to users who created those records. In other words, everyone manages their data and doesn't see anyone else's data. This article will show you how to implement it in the most simple way, with a single database, in a Filament admin panel.

In fact, this approach comes from general Laravel, so can be implemented in any Laravel project, we're just showcasing the Filament example.


What We're Creating Here

Imagine a web project to manage Tasks, and every user may see only their own entered tasks.

tasks app


Step 1. Logging User Who Created The Record

For all the models that we want to be multi-tenanted, we need an extra field in their DB table.

php artisan make:migration add_user_id_to_tasks_table

And then migration code:

return new class extends Migration {
public function up(): void
{
Schema::table('tasks', function (Blueprint $table) {
$table->foreignId('user_id')->nullable()->after('password')->constrained();
});
}
};

And then app/Models/Task.php addition - see the last line:

class Task extends Model
{
use Multitenantable;
 
protected $fillable = [
'title',
'user_id'
];
}

Now, how do we fill in that field automatically? We could do that with Model Observer, but for this case let's create a Trait, which you will be able to reuse for other Models.

We're creating a new folder app/Models/Traits and file app/Models/Traits/Multitenantable.php:

trait Multitenantable
{
protected static function bootMultitenantable(): void
{
static::creating(function ($model) {
$model->user_id = auth()->id();
});
}
}

Notice: Laravel doesn't have make:trait Artisan command, you need to create the file manually.

Notice the method name bootMultitenantable() - name convention bootXYZ() means Laravel will automatically launch this method when a trait is used. You can call it a trait "constructor".

So, what we're doing here? We add authenticated user ID to the field user_id, in whatever Model that is.

Notice, nowhere here do we mention Task or any other model name. So, we can add this trait to however many models we want.

Let's add this Trait - just two new lines of code in app/Models/Task.php:

use App\Models\Traits\Multitenantable;
 
class Task extends Model
{
use Multitenantable;
// ...
}

And that's it, field user_id will be filled in automatically when you create a task.

tasks db values


Step 2. Filtering Data By User

The next step is filtering data whenever someone accesses the Tasks list or tries to get the Task by its ID field.

We need to add Global Scope on all the queries on that model. And it's also possible to do it flexibly in the same app/Models/Traits/Multitenantable.php. Here's the full code:

use Illuminate\Database\Eloquent\Builder;
 
trait Multitenantable
{
protected static function bootMultitenantable(): void
{
static::creating(function ($model) {
$model->user_id = auth()->id();
});
 
static::addGlobalScope('created_by_user_id', function (Builder $builder) {
$builder->where('user_id', auth()->id());
});
}
}

We've included Eloquent/Builder class here, and then used static::addGlobalScope() to filter any query with the currently logged-in user.

That's it, every user will see only their tasks, but it will also work for other Models in the future if they use the same Trait.


Step 3. Administrator To See All Entries?

Some "super user" of the system should still see all tasks, right? Now, this functionality will depend on your user-role-permission implementation, but generally, I will show you the place where you can add this permission.

In the Trait's filter, we should add this if-statement:

trait Multitenantable
{
protected static function bootMultitenantable(): void
{
static::creating(function ($model) {
$model->user_id = auth()->id();
});
 
if (! auth()->user()->is_admin) {
static::addGlobalScope('created_by_user_id', function (Builder $builder) {
$builder->where('user_id', auth()->id());
});
}
}
}

Yes, it's that simple - just add an if-statement for those users who are exceptions for the filter.


Step 4. Filament Column

In this tutorial, we use Filament admin. It would be nice if the admin user would see who the task belongs to, in the table. It's very easy: for the desired column you just need to use the visible() method.

app/Filament/Resources/TaskResource.php:

class TaskResource extends Resource
{
// ...
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('title')
->searchable()
->sortable(),
TextColumn::make('user.name')
->searchable()
->sortable()
->visible(fn () => auth()->user()->is_admin),
]);
}
// ...
}

In this case, don't forget to add a relation for the Task model.

app/Models/Task.php:

use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Task extends Model
{
use Multitenantable;
 
protected $fillable = [
'title',
'user_id'
];
 
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

So, this is how easy it is to implement multi-tenancy in Laravel in a simple way, with Filament.

But there are more complex scenarios, and I showed them in my 2-hour course Laravel Multi-Tenancy: All You Need To Know.


If you want more Filament examples, you can find more real-life projects on our FilamentExamples.com.

avatar

Thank you so much for this great explanation, Please add more Filament tutorials. It really makes bulding admin dashboard easir espilalcy for newbie like me.

🥳 3
😍 3
avatar

Suggest topics :)

avatar

I shared some ideas with Povilas in Twitter, I hope we can see some advanced tutorials soon. https://twitter.com/ukcbmk1/status/1635319511359582209?t=sZuwCOkLQvl-js0M8rqH_g&s=19

avatar
Wemerson Couto Guimarães

Amazing article! Congrats!

An effective solution for a simple multitenancy structure.

avatar
William Gérald Blondel

Thank you for the tutorial!

However, user_id may not be an ideal name if the person creating the task is not the person to whom the task will be assigned..

avatar

I am adding user_id in users table, but it show me error 500, do you kno why ?

and I need to add this check so my seeder can work properly

if (auth() != null && auth()->user() != null) {
...
}
avatar

When using Seeders, they are usually launched from Terminal, which of course doesn't have logged in user or session at all.

I think you need to restructure on how you make seeders or this check, so give more details on what exactly you're trying to seed, and maybe we'll be able to help.

avatar
if (! auth()->user()->is_admin) { 
	static::addGlobalScope('agent_id', function (Builder $builder) {
	$builder->where('agent_id', auth()->id());
});
}

I have infinite loop issue. I want that the user would be able to create another user and then show only users he created. I'v got infinity loop. I think that auth()->id() is the problem, because it's querying the user model on boot. Can You point me in the right direction how to solve that ?

avatar

In my eyes your global scope looks good. Don't see why it should go to infite loop. Just changing auth()->id() to a hardcoded value does it fix the loop? Or even removing jus this line.

avatar

It still doesn't work. There are some problems similar to this on SO: https://stackoverflow.com/questions/56487694 or laracast: https://laracasts.com/discuss/channels/eloquent/using-data-from-authuser-in-global-scope. For now, I solved it by placing this code in UserResource:

public static function getEloquentQuery(): Builder
    {
        return static::getModel()::query()
            ->when(!auth()->user()->is_admin, function (Builder $query) {
                $query->where('agent_id', auth()->id());
               });
    }

Hope it helps someone too, although if anyone knows a better way to do this please let me know :)

avatar

Maybe we can move belongsTo relation method to the trait?

avatar

Yes, if you feel it will be repeating in multiple models.

avatar

namespace App\Http\Traits ;

use Illuminate\Database\Eloquent\Builder;

trait FilterByTeam {

protected static function boot()
{
    parent::boot();

    self::creating( function($model) {
        $model->team_id = auth()->user()->current_team_id ;
    });

    self::addGlobalScope( function( Builder $builder) {
        $builder->where('team_id', auth()->user()->current_team_id );
    });
}

}

avatar

It seems like there is a mistake in the acticle. You create migration add_user_id_to_tasks_table but then you write a migration text for adding "is_admin" to users table

avatar

Thanks for flagging it, fixed now.

avatar

Thanks Brother you helped me.. Love from India...

                                                                                                                          Dragon
👍 1
avatar

is this working with v3 (i don't like the default tenancy of v3) ?

avatar

I don't like tenancy in v3 either :) And yes it should work in v3.

avatar

I can't implement this scope realization, because the scope constructor executes very early, before the auth middleware has run. You cannot access the currently authenticated user in the boot method of the Eloquent model - the Auth middleware hasn't run yet at this point. To fix this problem i used Query Scopes

Like our articles?

Become a Premium Member for $129/year or $29/month
What else you will get:
  • 60 courses (1085 lessons, total 42 h 44 min)
  • 80 long-form tutorials (one new every week)
  • access to project repositories
  • access to private Discord

Recent Premium Tutorials