php 110 lines · 4 tabs

Enforce Multi-Tenant Isolation in Laravel with a Global Scope Bound to the Current User

Shared by codesnips Aug 2026
4 tabs
<?php

namespace App\Tenancy;

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 = TenantContext::id();

        if ($tenantId === null) {
            return;
        }

        $builder->where(
            $model->qualifyColumn($model->getTenantColumn()),
            $tenantId
        );
    }
}
4 files · php Explain with highlit

This snippet shows how to enforce row-level tenant isolation in a Laravel application by binding an Eloquent global scope to the currently authenticated user's tenant_id. The pattern solves a recurring SaaS problem: every query against tenant-owned tables must be silently constrained to the caller's tenant, so a forgotten where('tenant_id', ...) cannot leak another customer's data. Instead of trusting each query author, the constraint is applied centrally and automatically at the model layer.

The TenantScope tab implements Scope::apply, adding a where on the model's qualified tenant_id column whenever a resolvable tenant exists. Resolution goes through a TenantContext container rather than reaching into Auth directly, which keeps the scope usable from queue jobs and console commands where there is no authenticated request. When no tenant is bound, the scope leaves the query untouched, and a matching guard in the model raises rather than allowing an unscoped read in tenant-required paths.

The BelongsToTenant trait wires everything together in bootBelongsToTenant: it registers the global scope and hooks the creating event so new records inherit the current tenant_id automatically, preventing rows from being written without an owner. It also exposes forgetTenant via withoutGlobalScope(TenantScope::class) for the rare, deliberate cross-tenant query such as an admin report — making the escape hatch explicit and greppable.

The TenantContext container is a singleton holding the active tenant id, with runAs allowing a temporary override that is always restored in a finally block, which matters for jobs that process work on behalf of many tenants in one worker process. Finally, SetTenantFromUser middleware binds the tenant from the authenticated user at the start of each request via TenantContext::set.

The main trade-off is that the isolation now depends on the context being populated; code paths that bypass middleware must call runAs explicitly. The upside is that a single missed where no longer becomes a data breach. Developers reach for this when correctness and security must be the default, not the responsibility of every individual query.


Related snips

Share this code

Here's the card — post it anywhere.

Enforce Multi-Tenant Isolation in Laravel with a Global Scope Bound to the Current User — share card
Link copied