Automatically assign tenant IDs and restrict Eloquent queries using a reusable BelongsToTeam model trait with model booting and global scopes.
In multi-tenant or team-based SaaS applications, models (such as projects, invoices, and documents) must belong to a specific team, and users must only access records belonging to their active team.
Instead of writing repetitive query scopes and $model->team_id = auth()->user()->team_id assignments in every controller, you can encapsulate multi-tenant behavior in a reusable model trait.
The BelongsToTeam Trait
namespace AppTraitsModels;
use AppModelsTeam;
use IlluminateDatabaseEloquentBuilder;
use IlluminateDatabaseEloquentRelationsBelongsTo;
trait BelongsToTeam
{
public static function bootBelongsToTeam(): void
{
// Automatically set the team_id when creating a new record
static::creating(function ($model): void {
if (auth()->check() && empty($model->team_id)) {
$model->team_id = auth()->user()->team_id;
}
});
}
protected static function booted(): void
{
parent::booted();
// Apply a global scope to filter queries by active team
if (auth()->check()) {
static::addGlobalScope('team', function (Builder $query): void {
$query->team();
});
}
}
public function scopeTeam(Builder $query): Builder
{
return $query->when(
auth()->user()?->team_id,
function (Builder $query, int $teamId): Builder {
// Prefix column with table name to prevent SQL ambiguity in joins
return $query->where($this->getTable() . '.team_id', $teamId);
},
function (Builder $query): Builder {
// If the user has no team, prevent data leakage
abort(403, 'User does not belong to an active team.');
}
);
}
public function team(): BelongsTo
{
return $this->belongsTo(Team::class);
}
}
Using the Trait on Models
namespace AppModels;
use AppTraitsModelsBelongsToTeam;
use IlluminateDatabaseEloquentModel;
class Project extends Model
{
use BelongsToTeam;
protected $fillable = ['name', 'description'];
}
What This Automates
- Automatic Assignment: Calling
Project::create(['name' => 'API v2'])automatically populatesteam_idfrom the authenticated user. - Automatic Query Scoping: Calling
Project::all()automatically generatesWHERE projects.team_id = ?. - Join Safety: Using
$this->getTable() . '.team_id'ensures joins andhasManyThroughrelationships do not encounter "ambiguous column" SQL errors.
Summary
- Use a
BelongsToTeamtrait to automate tenant ID assignment on creation and filter queries via global scopes. - Table-prefix column references in query scopes to prevent SQL errors in joins.
- Eliminates manual scoping across controllers and guarantees tenant isolation.
Tags:
Laravel Eloquent Multi-Tenancy Architecture Security