I built a multi-branch business system in Laravel. Each branch must only see its own invoices, clients, and payments. A global scope solved that in a few lines. Then the bugs started.
Here is the setup, and the four problems I had to fix.
The trait
trait BelongsToBranch { protected static function bootBelongsToBranch(): void { static::addGlobalScope('branch', function (Builder $query) { $user = auth()->user(); if (! $user || $user->isAdmin()) { return; } $query->where( $query->getModel()->getTable() . '.branch_id', $user->branch_id ); }); static::creating(function ($model) { if (auth()->check() && empty($model->branch_id)) { $model->branch_id = auth()->user()->branch_id; } }); } } Add use BelongsToBranch; to any model, and every query is filtered automatically.
Bug 1: "Column 'branch_id' is ambiguous"
My first version used where('branch_id', ...). It broke as soon as a query joined two tables that both had branch_id.
Fix: always qualify the column with the table name, as in the trait above.
Bug 2: Shared records disappear inside relations
Some clients are shared across branches. When a branch user opened an invoice, $invoice->client returned null, because the scope also ran on the related query.
Fix: remove the scope on that specific relation:
public function client() { return $this->belongsTo(Client::class) ->withoutGlobalScope('branch'); } The hard part is consistency. If you remove it in one place but not in an eager load or a whereHas, the same record shows on one page and vanishes on another.
Bug 3: Queued jobs see everything
Jobs and commands run without a logged-in user. My scope returned early when there was no user, so a queued report job read data from all branches.
Fix: never rely on auth() inside jobs. Pass the branch explicitly:
Invoice::withoutGlobalScope('branch') ->where('branch_id', $this->branchId) ->get(); Bug 4: Unique validation ignores the scope
Rule::unique('invoices', 'number') queries the table directly. Global scopes don't apply, so invoice number INV-001 in branch A blocked INV-001 in branch B.
Fix: add the branch condition yourself:
Rule::unique('invoices', 'number') ->where('branch_id', auth()->user()->branch_id) The same applies to raw DB::table() queries. Global scopes only work through Eloquent.
Takeaway
Global scopes are a clean way to isolate data, but they work silently. Test three cases every time: a branch user, an admin, and a queued job.
I build multi-branch and SaaS systems in Laravel. If you need one, you can find me here: [YOUR ADMIN PANEL / SAAS GIG LINK]