Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Tenant Aware Laravel Package

maize-tech/laravel-tenant-aware

maize-tech/laravel-tenant-aware helps Laravel apps support multi-tenancy by resolving the active tenant, automatically scoping Eloquent models and queries, and keeping tenant data isolated with simple configuration and middleware.

View on GitHub
Deep Wiki
Context7

Getting Started

maize-tech/laravel-tenant-aware adds multi-tenancy to a Laravel app with minimal surgery: you tag models with a trait, the package applies a global scope filtering every query by the current tenant and auto-fills the tenant foreign key on create.

composer require maize-tech/laravel-tenant-aware
php artisan vendor:publish --tag=tenant-aware-config

Then, in order:

  1. Read the config file first (config/tenant-aware.php). It defines which column/model is used to identify a tenant and how rows are tied together — this is the single source of truth for the rest of the integration.
  2. Add the tenant column to your tables — a nullable, indexed tenant_id (or whatever the config points at) on every tenant-scoped table. Prefer composite indexes like (tenant_id, created_at) and (tenant_id, email) from day one; retrofitting indexes after the fact is the expensive part.
  3. Apply the trait to your Eloquent models. Models without the trait are global/shared — use that deliberately for lookup tables (countries, plans, roles).
  4. Set the tenant context early in the request lifecycle — a middleware that resolves the tenant (subdomain, auth user, header, session) and pushes it into the package before any authenticated controller runs.

First use case: a single-tenant-per-user SaaS. Add tenant_id to posts, users, invoices; put the trait on those models; add middleware that resolves the tenant from the authenticated user's team and calls the package's setter. From that point Post::all() is automatically scoped, Post::create([...]) is automatically stamped, and you have not written a single where().

If you are upgrading from 2.1.x: 2.2.0 requires PHP 8.2+ (8.1 support dropped) and adds Laravel 13 support. Bump your CI matrix and composer.json platform constraint before anything else.

Implementation Patterns

Tenant resolution in middleware, not in controllers. Resolve once, set once, and let the rest of the stack be ignorant of tenancy:

final class SetTenant
{
    public function handle($request, Closure $next)
    {
        $tenant = $request->user()?->tenant
            ?? Tenant::where('domain', $request->getHost())->firstOrFail();

        TenantAware::setTenant($tenant);
        try {
            return $next($request);
        } finally {
            TenantAware::forgetTenant(); // critical for Octane / long-running workers
        }
    }
}

Cross-tenant admin screens. Use the package's bypass/withoutTenant()-style helper rather than manually stripping scopes on every model. Wrap the whole scope in a closure so it cannot leak:

$allInvoices = TenantAware::withoutTenant(fn () => Invoice::with('tenant')->paginate());

Queue jobs. Serialisation does not carry tenant context. Capture the tenant id on the job, re-hydrate at the start of handle(), and clear it in a finally:

public function handle(): void
{
    TenantAware::setTenant(Tenant::findOrFail($this->tenantId));
    try { $this->report->generate(); }
    finally { TenantAware::forgetTenant(); }
}

Console commands and scheduled tasks. Iterate tenants explicitly; there is no request to resolve from:

Tenant::eachById(function (Tenant $tenant) {
    TenantAware::setTenant($tenant);
    $this->call('invoices:remind');
});

Route model binding and validation. Scoped queries mean Route::get('/invoices/{invoice}') returns 404 for another tenant's record for free — a genuine security win, and worth a feature test. For uniqueness, remember unique:invoices,number is now wrong; use Rule::unique('invoices')->where('tenant_id', tenant()->id).

Testing. Use RefreshDatabase, and in setUp()/tearDown() always set and clear the tenant. Build factories that accept a tenant and default it to a fresh one. Write one explicit test per model asserting that a second tenant's rows are invisible — that single test is what stops a silent data-leak regression after a refactor.

Seeding and onboarding. New-tenant provisioning should run inside a tenant context so seeds auto-stamp: TenantAware::setTenant($tenant) then run the seeder. Shared plans/roles tables stay untenant-scoped and are seeded outside.

Gotchas and Tips

  • Empty results are indistinguishable from "no permission". A forgotten setTenant() in a job or command does not throw; it quietly returns zero rows. Log the active tenant id on every request/job, or add a boot-time assertion in non-production environments.
  • Context leaks between requests. Under Octane, Swoole, or any queue daemon (horizon, queue:work), the facade/singleton holds state across jobs. Always pair setTenant() with a finally { forgetTenant(); }. Laravel's default queue:work is long-lived — this is a real bug, not a theoretical one.
  • Raw queries and DB:: bypass everything. DB::table('invoices'), whereRaw, and some third-party packages silently ignore the scope. Grep for DB::table on tenant tables and add explicit where('tenant_id', ...). Report/aggregate builders are the usual offenders.
  • Relations go through scopes too. $invoice->lines is fine if InvoiceLine carries the trait; if it doesn't, you get cross-tenant rows through the parent's key path. Audit every relationship target.
  • Mass assignment. Auto-fill happens on create, but if tenant_id is fillable and present in $request->all(), a client can stamp a row into another tenant. Either keep it out of $fillable entirely (the package fills it) or validate it against the active tenant.
  • Caching and storage aren't tenant-aware. Cache keys must be prefixed (tenant:{$id}:...), and Storage paths/flysystem disks must be per-tenant. The package scopes the database only — everything else is yours.
  • Migrations must be ordered. Adding tenant_id as non-nullable with a foreign key on a table that already has rows requires an explicit backfill step; do it in three migrations (add nullable → backfill → make non-nullable + index), not one.
  • Upgrading to 2.2.0. The only functional change is platform support (Laravel 13 added, PHP 8.1 dropped). No API changes — verify with a composer why-not php 8.1 style check in CI and confirm your laravel/framework constraint allows the new major before adopting.
  • Extension points. If the default resolution doesn't fit (e.g. a tenant id on the JWT rather than a model relation), prefer a custom resolver and a custom scope over sprinkling withoutGlobalScope() calls through controllers — the latter erodes the guarantee that makes the package worth using.
  • Debugging a suspiciously empty page. Check in this order: tenant set in middleware? model has the trait? the query goes through Eloquent, not DB::? a stale withoutGlobalScope() left in a service class? That checklist resolves most "the package is broken" reports.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console