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 Multitenant Laravel Package

solutosoft/laravel-multitenant

A Laravel package for building multi-tenant applications with tenant resolution and tenant-aware database connections, models, and migrations. It helps isolate tenant data across domains or subdomains while keeping a single codebase.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. composer require solutosoft/laravel-multitenant — before you do, open the package's composer.json and confirm the illuminate/* constraint covers your Laravel version. The last release is from March 2024, so Laravel 11/12 apps should test on a branch first.
  2. The service provider is auto-discovered. Publish the config (php artisan vendor:publish --provider="Solutosoft\Multitenant\MultitenantServiceProvider") and read config/multitenant.php end to end — it's short and defines the tenant model class, the route parameter name used to resolve the tenant, and the foreign key column convention ({tenant}_id).
  3. Mark your tenant model (usually Account, Team, or Organization) with the tenant trait (Multitenantable). This makes it the thing the middleware resolves.
  4. Mark every model that belongs to a tenant with the scoped trait (Solutosoft\Multitenant\BelongsToMultitenant). This registers a global scope on account_id and fills the foreign key on create().
  5. Register Solutosoft\Multitenant\Http\Middleware\Multitenant on your tenant routes (not globally — see Gotchas), typically after auth.
  6. First use case: a route like Route::get('/accounts/{account}/posts', ...). Inside the controller, Post::all() returns only that account's posts and Post::create([...]) gets account_id injected for you.

Where to look first: the src/ directory is small enough to read in twenty minutes. Do that rather than relying on the README — the trait names, the scope class name, and the "no tenant resolved" behaviour are the three things you need to know exactly.

Implementation Patterns

Two resolution strategies, pick per route group. For a staff/admin surface, resolve tenancy from the URL (/accounts/{account}/...) so people can switch tenants by navigating. For an end-user surface, resolve from the authenticated user's tenant so URLs stay clean. The middleware supports both; don't mix them in the same route group without thinking about it.

Nested resources. Child routes under {account} inherit tenancy from the parent parameter automatically, but the tenant model's own route binding is not scoped — a user can hit /accounts/{other_account}/posts and get an empty list rather than a 404. Add an explicit authorization check (policy or a Gate::before) on the tenant parameter, because the global scope silently hides the leak instead of failing loudly.

Cross-tenant / backoffice queries. Use Post::withoutGlobalScope(MultitenantScope::class) for reporting, admin dashboards, and export jobs. Wrap these in a clearly named repository method (Post::forAllTenants()) so they're greppable — ad-hoc withoutGlobalScope calls scattered through controllers are how tenant isolation bugs get shipped.

Queues and scheduled tasks. Context does not survive serialization. Capture the tenant id into the job payload in the constructor, and in handle() re-establish context (set the tenant model / call the resolver) inside a try/finally that resets it. For scheduled commands, loop tenants explicitly and set context per iteration — never rely on the previous iteration's state.

Migrations. Every tenant-scoped table gets foreignId('account_id')->index() (or whatever your config uses) plus composite unique indexes: unique on ['account_id', 'slug'], ['account_id', 'email'], etc. A global unique index on email is a bug the moment a second tenant signs up.

Factories and seeders. Models created by factories still go through the model events, so account_id gets filled — but only if there's a resolved tenant. In tests, create the tenant first, then actingAs($user) or hit the route so the middleware runs. Factories that run outside a request (seeders, php artisan tinker) need the tenant set explicitly.

Tests. For every tenant-scoped resource, write the two-tenant test: create records under A and B, authenticate as A, assert B's record is 404 and A's is 200. It's ten lines and it's the only thing that reliably catches a missing trait or a missing middleware.

Keep a thin seam. Extend the package traits in your own App\Models\Concerns\BelongsToTenant that composes the package trait. When you outgrow single-database tenancy (or the package goes stale), you swap one file instead of touching thirty models.

Gotchas and Tips

  • It fails open, not closed. If the middleware doesn't run, the scope has nothing to scope by. Find out exactly what the package does when no tenant is resolved — throws, or returns everything — and add a hard guard in your base model (throw new TenantNotResolvedException in a booted() check or a scope wrapper). This is the single most important thing to verify on day one.
  • Middleware ordering vs. route model binding. If the tenant is resolved from a route parameter that is itself a bound model, the tenancy middleware must run before SubstituteBindings. Check your $middlewarePriority if you see the tenant resolved too late or not at all.
  • Raw queries bypass everything. DB::table('posts'), DB::select(...), and raw whereRaw joins have no global scope. Audit for these; they're the usual source of "why can tenant A see tenant B's rows in the export?"
  • create() fill vs. mass assignment. The trait sets the foreign key via a model event. If you've added account_id to $fillable, a malicious payload can set it explicitly — check whether the event overwrites an explicit value (it usually does, but confirm) and consider leaving it out of $fillable entirely.
  • Soft deletes compose. Both global scopes apply; a withTrashed() call doesn't remove the tenant scope, which is usually what you want but surprises people during restore operations.
  • Long-running processes. Octane, Horizon, and queue workers keep the container alive. Reset tenancy context in finally blocks, or you'll get a worker processing tenant B's job under tenant A's scope — a silent and very bad data leak.
  • Config cache. php artisan config:cache after publishing; in deployment pipelines this is a classic "works locally, wrong tenant in prod" failure.
  • Debugging the scope. ->toSql() or a DB::listen() dump is the fastest way to confirm where account_id = ? is actually being appended. If that clause isn't in the SQL, nothing else matters.
  • Bus factor. 24 stars and a single maintainer means you are effectively the maintainer. Pin to an exact version in composer.json, vendor-read the scope and trait classes, and keep your own concerns-layer wrapper so a swap to stancl/tenancy or hand-rolled scopes is a contained change. Budget for that conversation now rather than during an incident.
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.
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
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource