<?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);
});
}
}
<?php
namespace App\Models\Concerns;
use App\Models\Scopes\TenantScope;
use App\Models\Tenant;
use App\Support\TenantContext;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
trait BelongsToTenant
{
public static function bootBelongsToTenant(): void
{
static::addGlobalScope(new TenantScope());
static::creating(function ($model): void {
if (empty($model->tenant_id)) {
$model->tenant_id = app(TenantContext::class)->id();
}
});
}
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
}
<?php
namespace App\Support;
class TenantContext
{
private ?int $tenantId = null;
public function set(?int $tenantId): void
{
$this->tenantId = $tenantId;
}
public function id(): ?int
{
return $this->tenantId;
}
public function has(): bool
{
return $this->tenantId !== null;
}
public function forget(): void
{
$this->tenantId = null;
}
}
<?php
namespace App\Http\Middleware;
use App\Support\TenantContext;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SetTenantContext
{
public function __construct(private TenantContext $context)
{
}
public function handle(Request $request, Closure $next): Response
{
$user = $request->user();
if ($user !== null && $user->tenant_id !== null) {
$this->context->set($user->tenant_id);
}
return $next($request);
}
}
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
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.