Multi-Tenant Security: Eloquent Isolation and Tenant-Aware Scopes
Master multi-tenant security by implementing robust row-level isolation in Laravel. Learn to build tenant-aware Eloquent scopes that prevent cross-tenant leaks.
Previously in this course, we explored JWT and Stateless Security to manage identity. Now, we must ensure that once a user is authenticated, they cannot access data belonging to another customer. In a shared-database architecture, "security by convention" is insufficient; you need a system that enforces isolation at the database query level.
The Principle of Implicit Isolation
In a multi-tenant application using a shared database, every table typically contains a tenant_id column. The primary risk is a developer forgetting to add ->where('tenant_id', $currentTenantId) to a query, which exposes sensitive information to unauthorized users.
We solve this by moving from explicit filtering to Global Scopes. By binding the tenant context to the application lifecycle, we force Eloquent to append the necessary constraints to every SELECT, UPDATE, and DELETE operation automatically.
Implementing a Tenant-Aware Scope
To implement this, we first need a contract or a trait that defines which models are "tenant-sensitive."
PHPnamespace App\Traits; use App\Models\Scopes\TenantScope; trait BelongsToTenant { protected static function bootBelongsToTenant() { static::addGlobalScope(new TenantScope); static::creating(function ($model) { if (is_null($model->tenant_id)) { $model->tenant_id = auth()->user()?->tenant_id; } }); } }
The TenantScope class then performs the heavy lifting by modifying the query builder instance:
PHPnamespace App\Models\Scopes; 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) { #6A9955">// Fallback to a service or request-bound tenant provider $tenantId = app('current_tenant_id'); if ($tenantId) { $builder->where($model->getTable() . '.tenant_id', $tenantId); } } }
Advancing the Project: Tenant Context Provider
In our ongoing project, we need a reliable way to resolve the current_tenant_id. Relying directly on auth() inside a scope can be fragile (e.g., during background jobs). Instead, we use a dedicated TenantManager service that is set during the request cycle via middleware.
- Create the Manager: Stores the ID in a singleton container.
- Middleware:
Extracts the tenant identifier (from a header or sub-domain) and calls
TenantManager::setTenant($id). - Integration:
The
TenantScopenow pulls fromapp(TenantManager::class)->getTenantId().
This architecture ensures that even if you forget to filter in your controller, the query will fail or return empty rather than leaking data. As discussed in Preventing BOLA Vulnerabilities in Multi-Tenant API Architectures, this is your primary defense against Broken Object Level Authorization (BOLA).
Hands-on Exercise
- Create a
TenantScopeclass that filters bytenant_id. - Apply the
BelongsToTenanttrait to yourProjectandTaskmodels. - Write a test case that attempts to fetch a
Taskby ID where thetenant_iddoes not match the currently set tenant. Assert that the result isnullor aModelNotFoundException. - Challenge: Implement a
withoutTenant()method in your scope to allow administrative access for cross-tenant reporting when explicitly required.
Common Pitfalls
- Background Jobs: If you don't explicitly pass the
tenant_idinto a queued job, theTenantManagerwill be empty, and your jobs will fail to find any records. Always serialize thetenant_idinto your job constructor. - Querying Across Tenants: Sometimes you need to aggregate data across tenants (e.g., global analytics). Using
Model::withoutGlobalScope(TenantScope::class)->get()is the standard way to bypass the filter, but ensure this is wrapped in strict authorization logic. - Performance: Ensure your
tenant_idcolumn is included in every composite index. As we covered in Advanced Indexing Strategies, filtering on an unindexed column will lead to full table scans as your tenant count grows.
Recap
Security in multi-tenancy is not an "add-on"—it must be baked into the Eloquent layer. By using Global Scopes, we eliminate the possibility of human error in controller-level queries. While this provides strong isolation, remember that it is only one layer of defense; you should still audit your API endpoints for BOLA risks as outlined in our security roadmap.
For environments requiring even stricter isolation, consider exploring Postgres RLS for Multi-Tenant SaaS, which moves the enforcement from the application layer down to the database engine itself.
Up next: We will discuss how to prevent Server-Side Request Forgery (SSRF) when your application needs to fetch resources from external URLs on behalf of different tenants.
Work with me

Laravel SaaS MVP & Multi-Tenant App Development
Launch your SaaS MVP on Laravel — multi-tenant, subscription-ready, and built by the engineer behind a platform serving 10,000+ paying users.

Laravel REST API Development
Clean, secure, well-documented Laravel REST APIs — the backend engine for your app, mobile client, or SaaS. Built by an API specialist.