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

Multitenant Plugin Laravel Package

muhammad-nawlo/multitenant-plugin

A Laravel package for adding multi-tenancy to your application, with tenant-aware database connections, middleware, and automatic model scoping to keep each tenant's data isolated. Simplifies building SaaS apps where one codebase serves multiple customers.

View on GitHub
Deep Wiki
Context7

Getting Started

Read src/ before you trust anything below. This is a ~1-star package with a 1.0.0 tag and no changelog. The class/config names here follow Filament v3 and stancl/tenancy v3 conventions but should be verified against vendor/muhammad-nawlo/multitenant-plugin/src/ on the exact version you install.

Prerequisites (in this order — the plugin is a UI layer, not a tenancy implementation).

  1. stancl/tenancy must already be installed, configured, and working from the CLI. The plugin wraps it; it does not replace it.
    composer require stancl/tenancy
    php artisan tenancy:install          # publishes tenancy.php, creates database/migrations/tenant
    php artisan migrate                  # central DB only
    
  2. Confirm your config/tenancy.php has the bootstrappers you want (DatabaseTenancyBootstrapper, CacheTenancyBootstrapper, etc.) and that tenant_model and domain_model are set.
  3. filament/filament v3 on a central panel only (see Gotchas — do not add this to a tenant panel).

Install.

composer require muhammad-nawlo/multitenant-plugin

Pin the exact version in composer.json ("muhammad-nawlo/multitenant-plugin": "1.0.0") rather than using ^1.0. With no release notes, a patch bump is an unaudited code change.

Publish whatever the package provides — check the provider's boot()/publishes() calls first, they may use a different tag name than the convention:

php artisan vendor:publish --provider="MuhammadNawlo\MultitenantPlugin\MultitenantPluginServiceProvider"
php artisan migrate

Register on the panel (app/Providers/Filament/AdminPanelProvider.php):

use MuhammadNawlo\MultitenantPlugin\MultitenantPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        ->id('admin')
        // ...
        ->plugin(MultitenantPlugin::make());
}

First use case — get one vertical slice working end to end. In the central admin panel: create a tenant → a domain row → run tenant migrations → log in as that tenant. Do not move on until all four steps work through the plugin's UI rather than through php artisan tinker. If the UI can create a tenant but not migrate it, you have found the plugin's scope boundary and you write that glue yourself.

Keep config/tenancy.php open in a second tab for the whole first session. Everything the plugin does resolves back to that file.

Implementation Patterns

Separate the central panel from the tenant panel. The plugin manages tenants, so it belongs on the panel that is not itself tenanted. Put tenant-facing application code on a second Filament panel whose routes carry the tenancy middleware:

// AdminPanelProvider — central
->plugin(MultitenantPlugin::make())
->middleware(['web', 'auth'])

// TenantPanelProvider — tenant-scoped
->middleware([
    'web',
    InitializeTenancyByDomain::class,   // stancl
    PreventAccessFromCentralDomains::class,
    'auth',
])

Wrap tenant creation behind your own service, never call the plugin's resource action directly from domain code. The plugin's value is the Filament UI; the lifecycle logic should be yours so it is reusable from seeders, console commands, and tests:

final class ProvisionTenant
{
    public function __invoke(string $id, string $domain, string $plan): Tenant
    {
        $tenant = Tenant::create(['id' => $id, 'plan' => $plan]);
        $tenant->domains()->create(['domain' => $domain]);

        $tenant->run(function () {
            Artisan::call('migrate', ['--path' => 'database/migrations/tenant', '--force' => true]);
            Artisan::call('db:seed', ['--class' => 'TenantSeeder', '--force' => true]);
        });

        return $tenant;
    }
}

Then register the same logic as a listener so UI-created tenants and programmatically-created tenants take an identical path:

Event::listen(TenantCreated::class, function (TenantCreated $event) {
    $event->tenant->run(fn () => Artisan::call('migrate', ['--force' => true]));
});

Extend the tenant form without forking the resource. Filament doesn't support resource inheritance cleanly, so prefer the plugin's exposed hooks (form schema/table actions) if they exist. If they don't, the pragmatic pattern is: disable the plugin's resource in config or via the panel, and hand-write a TenantResource in your app that copies the vendor one. Track that file in git and diff it against the vendor resource on every upgrade — this is the cost of a package with no extension contract.

Tenant switching / impersonation. Any "log in as tenant" action must re-bootstrap tenancy and re-authenticate in the tenant's session context. After calling tenancy()->initialize($tenant), redirect to a tenant-panel route rather than rendering in place, so middleware runs and the session is rebound to the tenant connection. Store the impersonator's id in the tenant session and gate it on a super_admin policy.

Path-based or subdomain routing. Match your config/tenancy.php middleware to your URL scheme — InitializeTenancyByDomain for subdomains/custom domains, InitializeTenancyByPath (with ->prefix('{tenant}')) for path-based. Mixing the two across the plugin's routes and your own is a common source of 404s on the tenant panel.

Async work. Any job that touches tenant data should use stancl's TenantAware trait. Do not assume the plugin sets this up — it manages tenant records, not your job payloads.

Gotchas and Tips

Two different "tenancy" concepts will collide. Filament v3 has panel-level tenancy (->tenant(Team::class)), and stancl/tenancy has its own. This package is built on stancl/tenancy. If your panels are configured with Filament's native ->tenant(), you now have two tenancy systems bootstrapping in the same request. Pick one. For a real multi-tenant SaaS with per-tenant databases, it's stancl/tenancy, and this plugin is only its admin UI.

Middleware order is load-bearing. Tenancy initialisation must run before session, auth, and any bootstrapper-dependent middleware — and before Filament's own panel middleware if you are on a tenanted panel. TenantCouldNotBeIdentifiedException bubbling up as a 500 almost always means a tenant route is missing the middleware, or the middleware is ordered after web.

Subdomain dev environments. tenancy.test will not resolve acme.tenancy.test unless you wildcard it. Use valet link + valet tld, /etc/hosts entries, or *.localhost (which many browsers resolve automatically). Add your central domain to tenancy.central_domains before debugging anything else — a missing central domain makes the tenant panel try to resolve the admin panel as a tenant.

Migrations are the sharpest edge. Tenant migrations live in database/migrations/tenant and only run through $tenant->run(...) or php artisan tenants:migrate. A "Create tenant" button that doesn't trigger migration produces a tenant that exists in the central DB and nowhere else, and the failure surfaces as a confusing table-not-found on first tenant login, not at creation time. Hook TenantCreated and let the event drive migration, not the button.

Queues hide tenant-context bugs. QUEUE_CONNECTION=sync runs jobs inside the current request's tenant context, so everything "works." Switch to a real queue connection in development and re-test: jobs without TenantAware will silently write to the central database. This is the single most common production incident in stancl/tenancy projects.

Cache and Redis bootstrappers are per-tenant. If you enable CacheTenancyBootstrapper, tag-based separation requires a taggable store. With file cache it silently degrades to shared cache — the kind of cross-tenant leak you will not notice until a customer does.

Deletion is destructive and may be one click. If the resource exposes a Filament DeleteAction, check whether it drops the tenant database or just the model row. Add a ->requiresConfirmation() with the tenant id typed in, and consider a soft-delete-first flow with a scheduled purge (php artisan tenants:purge style) rather than immediate destruction.

Debugging tenant context. Three checks resolve most reports:

tenancy()->initialized;      // bool — is tenancy active in this request?
tenancy()->tenant?->getTenantKey();
DB::connection()->getName(); // 'tenant' vs 'central' — which connection is live?

Also log config('tenancy.central_domains') and the resolved host at boot when diagnosing 404s.

The package is the liability, not stancl/tenancy. Treat every upgrade as a code review: composer update -W in a branch, then read the diff of vendor/muhammad-nawlo/ before merging. Because the plugin is small and its scope (tenant CRUD plus domains) is a thin slice of stancl/tenancy, the exit cost is low — write your own TenantResource and uninstall. Do that deliberately rather than in a panic when the package lags a Laravel major.

Extension-point inventory (check which exist in the version you pin, in this order of preference): published config for tenant_model / resource registration → Filament form/table schema hooks → TenantCreated/TenantUpdated/TenantDeleted event listeners → forking the resource. Anything you change by editing vendor/ is lost on the next composer update; a patch via composer.json repositories + patches (cweagans/composer-patches) is the only acceptable way to modify vendor code, and only with an upstream issue filed.

Keep the plugin on the central panel only, forever. It has no business running inside a tenant context — attaching it to a tenanted panel means its resource queries run against the tenant connection and will 404 or, worse, list the wrong tenants.

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings