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

Filament Tenancy Laravel Package

tomatophp/filament-tenancy

Filament Tenancy by TomatoPHP adds multi-tenancy to Filament panels: switch tenants, scope resources and queries per tenant, and manage tenant records with straightforward configuration for Laravel apps.

View on GitHub
Deep Wiki
Context7

Getting Started

Check the stack before you type composer require. v5.0.0 targets Filament 5, Livewire 4, Laravel 12 or 13, PHP 8.2+, and stancl/tenancy 3.9+. If the project is still on Filament 3, you need the v3 branch — that is a branch switch in composer.json, not a version constraint. Pin both tomatophp/filament-tenancy and stancl/tenancy: the package is a thin UI layer over stancl, so its release cadence mostly tracks Filament's, and v5 was a breaking release.

Know what you're actually adopting. This is not a tenancy implementation; it's a Filament admin surface (tenant + member resources, tenant-aware panels, routing, switchers) on top of stancl/tenancy. Two published config files are your real documentation:

  • config/tenancy.php — central/tenant connection names, tenancy_db_name resolution, bootstrappers, seeder_parameters
  • config/filament-tenancy.php — tenant model, panel routing, UI behaviour

Read config/tenancy.php in full before touching a panel provider. Most "why doesn't this work" answers are in there.

Install:

composer require tomatophp/filament-tenancy
php artisan vendor:publish --tag="filament-tenancy-config"
php artisan vendor:publish --tag="filament-tenancy-migrations"
php artisan migrate   # stancl's tenants + domains tables, on the CENTRAL connection

The v5 install command runs in-process and no longer caches routes/config, so skip the reflexive route:clear / config:clear afterwards.

Wire the tenant panel (confirm the exact plugin class name against the v5 README — it changed between lines):

->tenant(\TomatoPHP\FilamentTenancy\Models\Tenant::class, slugAttribute: 'slug')
->tenantMiddleware([...], isPersistent: true)
->plugin(FilamentTenancyPlugin::make())

First use case: the vertical slice, one tenant, end to end. Admin creates a tenant in the central panel → provisioning creates the database and runs tenant migrations → $tenant->run(...) writes the owner → visiting /app/{id} initialises the tenant → a resource query hits the tenant DB. Do not build features until this loop closes cleanly once. Everything else in the package is elaboration on this path.

Three facts to internalise on day one:

  1. The tenant id is whatever the operator typed in the "Unique ID" field. Since v5 it is preserved verbatim. It becomes the database name (or its prefix), the route segment, and the FK in domains. Never assume UUID or auto-increment.
  2. There is no hard database column. stancl stores it in the data JSON as tenancy_db_name. Any code reading $tenant->database must be rewritten to the accessor.
  3. Seeding is opt-in. New tenants are seeded only when tenancy.seeder_parameters.--class is set. Null means a silently empty database.

Implementation Patterns

$tenant->run() is the primitive — use it for everything tenant-scoped. v5 moved owner creation inside $tenant->run() precisely because it works on every database driver. Copy that decision:

$tenant->run(function () use ($data) {
    // seeds, imports, reporting, owner writes
});

Only drop to manual config() + DB::purge() + DB::reconnect() when the connection must outlive a callback (an artisan command iterating the whole fleet). Even then, end with tenancy()->end().

Split migrations by connection. database/migrations for central, database/migrations/tenant for tenant-scoped. Drive them with stancl's commands rather than Artisan::call:

php artisan tenants:migrate
php artisan tenants:migrate --tenants=acme
php artisan tenants:seed --tenants=acme

For more than a handful of tenants, wrap tenants:migrate in your own command that logs per-tenant failures and continues instead of aborting the batch halfway.

Put provisioning in one action class. Create the row → let the package provision → $tenant->run() to seed and configure → set the storage root → dispatch the welcome mail. The flow already shifted once between v4 and v5; when it shifts again you patch one class, not every call site. It matters more now that id is operator-supplied and must be validated before it reaches a CREATE DATABASE.

Queues: carry the tenant id in the payload. Classic failure: dispatch from a tenant panel, process in a long-lived worker still holding the previous tenant's connection.

public function __construct(public string $tenantId) {}

public function handle(): void
{
    tenancy()->initialize(Tenant::findOrFail($this->tenantId));
    // tenant-scoped work
    tenancy()->end();
}

Registration. v5's register component redirects to the package's own tenancy.* routes. If you have custom post-registration routing or a catch-all, check php artisan route:list for those names — overriding them breaks the redirect with no error until a user complains.

Local development. Path tenancy (/app/{tenant}) on localhost; wildcard *.test domains when you're close to production. Because id feeds the URL, validate it hard on the create form: lowercase, hyphenated, no reserved words, no leading digits. Otherwise you're debugging routing oddities and invalid database names at the same time.

Testing: use the shipped factory and Pest suite. The first smoke test should assert both halves of the boundary in one test:

tenancy()->initialize($tenant);

$this->assertDatabaseHas('posts', [...]);
$this->assertDatabaseHas('tenants', [...], 'central');

Add a second asserting $tenant->run() owner writes land in the tenant DB. That pair catches most upgrade regressions, because the failure mode is almost always "wrote to the wrong connection."

Owners and users: decide the model explicitly. v5 writes the owner through the tenant connection. So: is auth per-tenant database, or central with a tenant pivot? Whichever you choose, the model needs an explicit $connection, and getTenants() must resolve against the central connection regardless. This is the sharpest edge in v5 and the top source of "the owner exists but I can't log in."

Keep a TenantManager seam. Provisioning, switching, seeding — route them all through your own class. On a package with 62 stars and zero dependents you are the QA team; the abstraction costs an afternoon and turns every future upgrade into a diff in one file.

Gotchas and Tips

Seeding fails silently by design. Unset tenancy.seeder_parameters.--class means creation succeeds with an empty database. That was deliberate (the application DatabaseSeeder can no longer block tenant creation), but it manufactures a class of "why is my tenant empty" tickets. Set it, and keep tenant seeders independent of the central seeder so a broken app seeder can't cascade.

The Unique ID is forever. It's the tenant id, the route segment, and probably the database name. Make the field read-only after creation, or write an explicit rename command that renames the database and purges stale connections. Retrofitting this with live customers is genuinely painful.

config:cache still freezes the tenant connection. Any code path reading the tenant DB before tenancy()->initialize() gets a placeholder from cached config. Never trust a cached database value; always go through initialisation at runtime.

Octane and queue workers need explicit teardown. v5 doesn't remove the end-of-request cleanup requirement. Listen on RequestTerminated / JobProcessed, call tenancy()->end() and DB::purge() for the tenant connection. Filament caches panel and auth state in Octane too — if you can't guarantee cleanup, don't put Octane in front of tenant panels.

Filament's native tenancy is a separate contract. getTenants() / HasTenants is relationship-based and must be satisfied on the central connection, while the panel's data queries go to the tenant one. v5's owner-inside-run() change sharpens this — verify which connection getTenants() resolves against or you'll chase a phantom auth bug.

Cache, session, and files still need per-tenant isolation. Keep cache and session off the tenant connection (or on Redis with a per-tenant prefix) and give each tenant a storage root at storage/app/tenants/{id}. v5's notes don't touch any of this, so every v4 gap carries straight over.

Debugging checklist, in order:

  1. tenancy()->tenant — null means the middleware never initialised.
  2. tenancy()->tenant->id vs the URL segment — did the Unique ID survive creation intact?
  3. DB::connection('tenant')->getDatabaseName() — did the switch reach the PDO?
  4. DB::getDefaultConnection() and the model's $connection — do they agree?
  5. tenancy.seeder_parameters.--class — is the tenant empty because seeding never ran?
  6. Middleware ordering — Filament resolves tenant-aware components early; tenantMiddleware(..., isPersistent: true) must be registered before panel boot.

Extension points worth knowing. The published Tenant model and its domains relation (add plan_id, status, trial columns freely — never touch id); $tenant->run() as the driver-safe callback; stancl's bootstrappers, the clean place to inject storage roots or per-tenant cache prefixes; and the create/delete events — hook deletion there so removing a tenant drops its database, domain rows, and files rather than orphaning them.

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