• Home
  • Business
  • Global Scopes and Tenant-Aware Queries: Preventing Accidental Data Exposure
Global Scopes and Tenant-Aware Queries in Laravel

Global Scopes and Tenant-Aware Queries: Preventing Accidental Data Exposure

One forgotten where clause is all it takes for a customer to open a report and see another company’s invoices. At Acquaint Softtech, a software product engineering partner for SaaS teams, that single line is the multi-tenant bug we plan against first, because no amount of UI polish recovers the trust it costs.

Global scopes are the standard defence. They attach a tenant filter to every query a model runs, so developers do not have to remember it each time. This guide explains how to build them, where they quietly stop working, and how to prove your tenants stay apart.

The patterns come from Laravel platforms. Acquaint Softtech has shipped as an Official Laravel Partner, with 1,300+ projects, 70+ in-house engineers, and clients across the USA, UK, Europe, and Australia who get a dedicated team within 48 hours of a brief.

What a global scope is

A global scope is a query constraint that Laravel’s Eloquent ORM applies automatically to every query on a model. A tenant scope adds “where tenant ID equals the current tenant” to each select, update, and delete, so tenant filtering becomes the default rather than a habit.

Tenant-aware queries, defined

A tenant-aware query is any database query that can only return or change rows belonging to the tenant making the request. The goal is for every query in the application to be tenant-aware without anyone writing the filter by hand.

How accidental data exposure happens

Cross-tenant leaks rarely come from attackers. They come from ordinary code paths that skip the tenant filter, usually during a rushed feature, a background job, or a quick admin report.

Leak pathHow it happensGuard
Missing filterA new query forgets the tenant conditionGlobal scope on every tenant model
Raw query builderDB::table() bypasses Eloquent scopesCode review rule and static checks
Queued jobWorker runs with no tenant contextPass and restore tenant per job
Shared cacheCache key omits the tenantPrefix every key with tenant ID
Route binding/invoices/42 loads any tenant’s recordScoped binding returns 404

Building a tenant scope the right way

Build the tenant scope once, as a class that implements Laravel’s Scope interface, and apply it through a shared trait on every tenant-owned model. One implementation means one place to fix and one place to audit.

Register it in a trait.

Call static::addGlobalScope inside the trait’s boot method, or use the ScopedBy attribute in recent Laravel versions. Any model that uses the trait is filtered automatically, so onboarding a new model is a one-line change.

Fill the tenant ID on create

Filtering reads is only half the job. Hook the model’s creating event to set the tenant ID from the current context, so no record is ever saved without an owner. Teams that hire Laravel developers with multi-tenant experience treat this pairing of read filter and write fill as non-negotiable.

Where global scopes do not protect you

Global scopes protect Eloquent model queries only. Anything that reaches the database another way skips them silently, and these are the paths where most real leaks hide.

Query builder and raw SQL

DB::table() and DB::select() do not load Eloquent models, so no global scope runs. Laravel’s Rule::unique and Rule::exists validation rules also use the query builder, which means a uniqueness check can match another tenant’s data unless you add the tenant condition yourself.

Queues, commands, and schedulers

A queue worker or scheduled command starts with no HTTP request and no logged-in user, so there is no tenant to scope by. Serialize the tenant ID into every job and restore it before any query runs; a job that cannot resolve its tenant should fail loudly, not run unscoped.

Bypassing scopes safely

Some queries must cross tenants, such as platform analytics or support tooling. Laravel allows this with withoutGlobalScope for one scope or withoutGlobalScopes for all, and every such call is a deliberate hole in your isolation.

Make bypasses rare and visible.

Wrap cross-tenant access in a small, named service used only by internal admin code, and log each call. A simple search for withoutGlobalScope in code review then shows every place isolation is switched off.

Defence in depth at the database layer

Application scopes can be bypassed by code; database rules cannot. Adding a second layer of isolation in the database means a missed scope returns nothing instead of another tenant’s data.

Row-level security and composite keys

PostgreSQL row-level security lets the database itself filter rows by a tenant setting on the connection, so even raw SQL is constrained. On MySQL, composite foreign keys that include the tenant ID stop a record from linking to a parent owned by another tenant.

Testing for cross-tenant leaks

The only proof of isolation is a test that tries to break it. Create two tenants, sign in as the first, and assert that every list, search, export, and API endpoint returns none of the second tenant’s records.

Automate it across every endpoint.

Run the two-tenant check in CI for each route, and add a direct-ID test that requests a record from the other tenant and expects a 404. Teams that hire automation testers can generate these tests from the route list, so new endpoints are covered automatically.

FAQs

What is a global scope in Laravel?

A global scope is a constraint Eloquent applies automatically to every query on a model. In multi-tenant apps, it adds the current tenant filter, so developers never have to remember to write it, and every read, update, and delete stays inside one tenant.

Do global scopes apply to DB::table queries?

No. Global scopes only run on Eloquent model queries. DB::table, DB::select, and validation rules such as Rule::unique use the query builder directly, so you must add the tenant condition manually or route those queries through scoped models.

How do I keep tenant context in queued jobs?

Store the tenant ID on the job when it is dispatched, then restore the tenant before the job runs any query. If the tenant cannot be resolved, fail the job instead of letting it run without a scope.

Is it safe to use withoutGlobalScope?

It is safe when rare, deliberate, and visible. Limit it to internal admin or analytics code, wrap it in a named service, and log each call, so code review can find every place tenant isolation is intentionally switched off.

Are global scopes enough to isolate tenant data?

Not on their own. They cover Eloquent queries only. Pair them with database-level controls such as PostgreSQL row-level security or tenant-aware composite keys, and automated tests that try to read another tenant’s data.

How do I test for cross-tenant data leaks?

Seed two tenants, sign in as one, and assert that lists, searches, exports, and APIs never return the other tenant’s records. Add direct-ID requests that expect a 404, and run the suite in CI on every build.