ifds-oss / tenant-guard
Defence-in-depth multi-tenancy for Laravel applications that share a single database and schema.
Requires
- php: ^8.2
- illuminate/contracts: ^10.0|^11.0|^12.0|^13.0
- illuminate/database: ^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^10.0|^11.0|^12.0|^13.0
- illuminate/queue: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- mockery/mockery: ^1.6
- orchestra/testbench: ^8.20|^9.0|^10.0|^11.0
- phpunit/phpunit: ^10.5|^11.0|^12.0|^13.0
- spatie/laravel-multitenancy: ^3.0|^4.0
- stancl/tenancy: ^3.8
This package is not auto-updated.
Last update: 2026-08-31 02:09:06 UTC
README
Defence-in-depth multi-tenancy for Laravel applications that share one database and one schema.
In a shared-schema SaaS, every tenant's rows live in the same tables, separated only by a
tenant_id column. It is the cheapest tenancy model to run and the easiest one to leak. One
forgotten where() is a cross-tenant data breach.
Tenant Guard's answer is not a single clever scope — it is five independent layers, each of which fails closed on its own:
- Query Scope — every Eloquent read is constrained automatically.
- Write Guard — cross-tenant creates, updates and deletes are refused, and the tenant key is immutable.
- SQL Sentinel — catches raw SQL and
DB::table()calls before they execute. - Propagation — the tenant follows the request into queued jobs and, optionally, the cache.
- Static Audit — a command that flags models and tables missing tenant protection.
It sits on top of connection switchers like stancl/tenancy and spatie/laravel-multitenancy
rather than replacing them — see the interoperability section of the
usage guide.
Installation
composer require ifds-oss/tenant-guard php artisan tenant-guard:install php artisan migrate
use Ifds\TenantGuard\Concerns\BelongsToTenant; class Post extends Model { use BelongsToTenant; }
Route::middleware('tenant')->group(function () { Route::get('/posts', [PostController::class, 'index']); });
That is the whole integration. The controller needs no where() clause — Post::all() only ever
returns the current tenant's rows.
Documentation
The full guide — every layer, the resolver chain, the API, events, testing helpers, gotchas and architecture — lives in USAGE.md.
See CHANGELOG.md for release notes.
License
MIT. See LICENSE.