<?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
);
}
}
<?php
namespace App\Tenancy;
use Illuminate\Database\Eloquent\Builder;
trait BelongsToTenant
{
public static function bootBelongsToTenant(): void
{
static::addGlobalScope(new TenantScope());
static::creating(function ($model) {
if ($model->getAttribute($model->getTenantColumn()) === null) {
$tenantId = TenantContext::id();
if ($tenantId === null) {
throw new MissingTenantException(static::class);
}
$model->setAttribute($model->getTenantColumn(), $tenantId);
}
});
}
public function getTenantColumn(): string
{
return $this->tenantColumn ?? 'tenant_id';
}
public function scopeForgetTenant(Builder $query): Builder
{
return $query->withoutGlobalScope(TenantScope::class);
}
}
<?php
namespace App\Tenancy;
class TenantContext
{
protected static ?int $tenantId = null;
public static function set(?int $tenantId): void
{
static::$tenantId = $tenantId;
}
public static function id(): ?int
{
return static::$tenantId;
}
public static function runAs(?int $tenantId, callable $callback)
{
$previous = static::$tenantId;
static::$tenantId = $tenantId;
try {
return $callback();
} finally {
static::$tenantId = $previous;
}
}
}
<?php
namespace App\Http\Middleware;
use App\Tenancy\TenantContext;
use Closure;
use Illuminate\Http\Request;
class SetTenantFromUser
{
public function handle(Request $request, Closure $next)
{
$user = $request->user();
if ($user !== null && $user->tenant_id !== null) {
TenantContext::set((int) $user->tenant_id);
}
return $next($request);
}
}
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
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
#!/usr/bin/env bash
set -euo pipefail
export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"
Secrets management with environment isolation and Vault
<?php
namespace App\Providers;
use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;
Laravel service container and dependency injection
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";
const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";
JWT access + refresh token rotation (conceptual)
package files
import (
"context"
"time"
Presigned S3 upload URLs (AWS SDK v2)
{
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build"
},
Laravel mix/Vite for asset compilation
Share this code
Here's the card — post it anywhere.