php 108 lines · 4 tabs

Automatic Multi-Tenant Scoping in Laravel with a Global Scope and Auth Context

Shared by codesnips Jul 2026
4 tabs
<?php

namespace App\Models\Scopes;

use App\Support\TenantContext;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $tenantId = app(TenantContext::class)->id();

        if ($tenantId !== null) {
            $builder->where($model->qualifyColumn('tenant_id'), $tenantId);
        }
    }

    public function extend(Builder $builder): void
    {
        $builder->macro('withoutTenant', function (Builder $builder) {
            return $builder->withoutGlobalScope($this);
        });
    }
}
4 files · php Explain with highlit

Multi-tenant SaaS applications share one database across many customers, and the single most dangerous class of bug is a query that accidentally leaks one tenant's rows to another. Rather than sprinkling where('tenant_id', ...) across every controller and query — which is easy to forget — this snippet centralises tenant isolation in an Eloquent global scope that is applied automatically to every query on tenant-owned models.

The TenantScope tab implements Illuminate\Database\Eloquent\Scope. Its apply method reads the current tenant id from a TenantContext singleton and, when present, injects a where clause qualified with $model->qualifyColumn('tenant_id') so it stays correct across joins. It also registers a withoutTenant macro via extend, giving controlled escape hatches for genuinely cross-tenant work like admin dashboards or nightly aggregation — the bypass is explicit and greppable rather than accidental.

The BelongsToTenant trait wires the scope into any model through the bootBelongsToTenant convention that Eloquent calls automatically. Crucially it also hooks the creating event so new records inherit the active tenant_id without the caller passing it, closing the write-side gap: reads are scoped and writes are stamped. The tenant relationship is declared for convenience.

TenantContext is a tiny request-scoped holder registered as a singleton. Keeping the tenant id in one object — rather than reading auth()->user() deep inside the scope — decouples the model layer from the auth guard and makes the value easy to set from a job, a console command, or a test.

The SetTenantContext middleware bridges authentication and persistence: on each authenticated request it calls TenantContext::set with the user's tenant_id, so every subsequent query is bound to that tenant. The trade-off to understand is that global scopes are silent — a developer who forgets TenantContext in a queued job gets unscoped queries — so the context should be established at every entry point (HTTP, queue, CLI). Used consistently, this pattern makes tenant isolation the default and cross-tenant access the deliberate exception.


Related snips

Share this code

Here's the card — post it anywhere.

Automatic Multi-Tenant Scoping in Laravel with a Global Scope and Auth Context — share card
Link copied