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 Nova Multitenancy Laravel Package

henryavila/laravel-nova-multitenancy

Adds multitenancy support to Laravel Nova by scoping resources, queries, and panel data to the current tenant. It helps isolate Nova administration per tenant and provides configuration for tenant-aware resource handling, routing, and access control in multi-tenant applications.

View on GitHub
Deep Wiki
Context7

Getting Started

v3.3.0 is a compatibility release: the only change is Laravel 11 support (PR #11). There are no API changes, no deprecations, and no migration steps beyond bumping your framework constraint. If you are already on v3.2.0 and Laravel 10, there is nothing to do; if you are planning a Laravel 11 upgrade, this is the version that unblocks you.

Minimal setup path:

  1. Install — composer require henryavila/laravel-nova-multitenancy. On Laravel 11 this requires PHP 8.2+ and a Nova release that itself supports Laravel 11 (Nova 4.27+ or Nova 5). Confirm your laravel/nova constraint resolves before you start the framework upgrade, otherwise you will chase phantom dependency conflicts.
  2. Publish the config — php artisan vendor:publish --tag=nova-multitenancy-config, then read config/nova-multitenancy.php top to bottom. It is short, and every behaviour you will later debug (tenant resolution source, session key, redirect target after switching) is driven from there.
  3. Wire your models — your User model implements the package's HasTenants contract and uses the matching trait (this is what gives you $user->tenants and the "current tenant" relationship). Your Tenant model implements the IsTenant contract and its trait. These two contracts are the whole integration surface; everything else builds on them.
  4. Register the middleware — add the package's tenant-resolution middleware to your web group (or to the Nova middleware stack in NovaServiceProvider). Order matters: it must run after Authenticate/StartSession so it can read the logged-in user and the session.
  5. Look at the tenant switcher first — the package ships a Nova user-menu entry that lets a user hop between the tenants they belong to. That single UI affordance is the fastest way to confirm the whole chain (contracts → middleware → session → Nova) is wired correctly.

First real use case: a single-database, single-deployment SaaS where a users table has a many-to-many tenant_user pivot, and every domain table carries a tenant_id. You do not need separate databases to get value here — the package's job is answering "which tenant is this request in?" and exposing that answer to Nova and to your query scopes.

Implementation Patterns

Resolution strategy — pick one and stay on it. The common configurations are (a) subdomain/domain-based, where the tenant is derived from the host, and (b) session-based, where the user picks a tenant from the switcher and it is persisted. Session-based is what most Nova-first apps want, because Nova is an admin panel and admins frequently belong to more than one tenant. Domain-based is better when tenants are externally addressable. Mixing them produces the classic bug where a user lands on acme.example.com but the session still says globex.

Scoping queries. The reliable pattern is a global scope on your tenant-owned models that reads the current tenant from the package (via its facade, e.g. NovaMultiTenancy::getTenant()) and applies where('tenant_id', ...). Do this on the Eloquent model, not in NovaResource::indexQuery(), so that non-Nova code paths, policies, and relationship counts are scoped identically. Nova resources then need no per-resource tenancy logic at all.

Resource visibility per tenant. Where tenants get genuinely different Nova experiences (different tools, cards, or resources), gate registration in NovaServiceProvider::tools()/resources() on the current tenant rather than hiding things client-side. Be aware that Nova caches its resource/tool manifest — flush php artisan nova:cache (or your configured cache) after changing tenant-conditional registration, or you will see the wrong menu for the wrong tenant.

Switching tenants in a request. A controller action that validates the user actually belongs to the requested tenant ($user->tenants()->whereKey($id)->exists()), calls the package's set-tenant method, and redirects back. Never trust a tenant id from the request without that membership check — this is the single most important authorization boundary in a multi-tenant admin panel.

Background work. Anything queued or scheduled has no HTTP session, so the tenant must be carried explicitly: put the tenant id on the job payload and set the context in handle() before touching models. The package exposes a "run this closure for tenant X" helper — wrapping job bodies in it is cleaner than manual set/forget pairs because it restores the previous context for you.

Artisan commands. Tenant-sensitive commands should take a --tenant= option and iterate tenants rather than running against an unscoped query. If you forget, a "cleanup old records" command will happily delete across every tenant.

Testing. Set the tenant in setUp() (or a Pest beforeEach) via the package's setter, and build a TenantFactory with per-tenant user factories. Assert the negative case too: user A of tenant 1 gets a 404/403 when fetching tenant 2's record by ID. Those tests are cheap and catch the scoping regressions that global scopes silently introduce.

Combining with stancl/tenancy or similar. This package is a Nova-layer tenant context, not a database-per-tenant bootstrapper. If you run database-per-tenant, use the other package for the actual switching and use this one only to make Nova tenant-aware; do not let both own the "current tenant" concept.

Gotchas and Tips

  • Long-running processes leak tenant context. Under Octane, Horizon, or queue workers, the tenant set during one request/job survives into the next. Always try/finally around the setter, or use the package's run-for-tenant helper which does the restore for you. Symptom: the first job after a deploy is fine, the tenth writes into the wrong tenant.
  • Cache keys must be tenant-aware. A tenant-scoped query is useless if Cache::remember('stats') returns the previous tenant's payload. Either prefix keys with the tenant id or use tenant-tagged stores, otherwise, you will only notice in production.
  • Middleware order is load-bearing. The resolver needs the authenticated user and the session. Registered before StartSession or before Nova's auth middleware, it will silently resolve to null and every model will look unscoped — which reads as "tenancy is broken" rather than "middleware is misplaced." When debugging, dd()/log the current tenant in a Nova route first.
  • Unique indexes fight multitenancy. A global unique on users.email prevents the same person existing in two tenants; and a global unique on tenant-owned slugs breaks the second tenant. Use composite uniques (tenant_id, email). This is a schema decision that is expensive to reverse.
  • Policies need the tenant dimension. A policy that checks "is this user an admin" is not enough — check "is this user an admin of this record's tenant". Centralise that as a belongsToCurrentTenant() check rather than repeating it in every policy.
  • Config cache. After editing config/nova-multitenancy.php in any environment with config:cache, run php artisan config:clear. A stale cached config is the usual cause of "I changed the session key and nothing happened."
  • Upgrading to v3.3.0. Treat it as a routine minor bump, but on Laravel 11 confirm: (a) Nova version supports L11, (b) no transitive package pins illuminate/* to ^10, (c) your config/nova-multitenancy.php was re-published or diffed — the L11 release did not announce config changes, so an unmodified published config should remain valid, but diff it against the package's stub anyway. Run php artisan nova:cache and your tenancy test suite afterwards.
  • Extension points. The two contracts on User and Tenant are the intended customisation seam, along with the published config. If you need custom resolution (header-based, JWT claim, impersonation by a super-admin), implement it inside the middleware/config rather than patching the vendor package — the latter will bite you on the next Laravel bump like this one.
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