leek/filament-subtenant-scope
Adds second-level tenancy to Filament panels: a topnav dropdown scopes every Eloquent query—resources, widgets, badges, and global search—to a sub-tenant like a service area, region, or branch. One trait plus a scopes array, no per-resource code needed.
Architecture fit. The package sits on a narrow, well-chosen seam: a Filament panel plugin that renders a topbar selector, resolves an active selection through a request-scoped manager (URL param → session → optional user storage), and attaches an Eloquent global scope to opted-in models during panel boot(). That last decision is load-bearing. Once any resource on a model declares $subTenantScopes, every query on that model inside that panel filters — list pages, relation managers, widget aggregates, nav badges, global search. This is a working-context convenience, not a data-isolation mechanism, and the README is honest about it. For "show me only the North service area" it fits well. For anything implying contractual or regulatory separation (per-entity compliance, billing boundaries, audit segregation) it is the wrong tool and should be rejected on product grounds before engineering spends a day on it.
Integration feasibility. Mechanically cheap: one composer require, one ->plugin() registration, one CSS @source directive in the panel theme (plus a npm run build), and per-resource a trait plus a static FK map. The scopeSubTenant{Key} escape hatch covers the common "profile → many areas" relational shape without forking. UI is overridable three ways (render hook, withoutDropdown(), publishable blade). Realistic spike: 1–3 days for a Filament-fluent engineer on a single scope; the cost is not installation but the authorization and audit work around it.
Technical risk.
optionsQuery constrains what the dropdown offers; the docs do not state that a supplied ?scope_<key>=<id> is validated against that same set. If it isn't, a crafted link is a horizontal-probe vector, softened only by Filament's own tenant scope and by global-scope ordering (an ID from another tenant may still collapse to zero rows — or may not, if the scope ordering puts the subtenant filter first).whereHas, and withoutGlobalScopes() call sites.Key questions.
optionsQuery for the current user and tenant?withoutGlobalScopes()?All option too?Stack fit. Requires PHP 8.2+, Filament v4/v5, Livewire v3/v4; CI covers PHP 8.2–8.4 × Laravel 11–13. The one non-Composer coupling is Tailwind: the panel theme must add @source '../../../../vendor/leek/filament-subtenant-scope/resources/views/**/*.blade.php' or responsive utilities in the dropdown get purged and the label collapses. That is a build-pipeline dependency — a front-end change to the theme can silently break the selector, so it belongs in the theme's documented contract, not in a README footnote someone read once.
Migration path. Nothing to migrate — the package is purely additive. The real work is subtractive: any existing hand-rolled "area filter" (a Filament table filter, a getEloquentQuery() override, a session-backed where) must be removed or explicitly reconciled, or you get double-filtering that looks correct in the happy path and returns empty sets when the two disagree. Grep for service_area_id, region_id, branch_id in resource query overrides before enabling anything.
Compatibility. v1.x, MIT, semver-shaped with a clean CHANGELOG through v1.1.1. Filament v5 and Livewire v4 are claimed but recent; pin exact versions and re-run the spike after any Filament major. Low stars and zero dependents mean we are effectively the first production reference — treat "works on the README's example" as unverified until our own test suite covers it.
Sequencing.
optionsQuery is rejected (or wrap the plugin's resolution to enforce it). Do not ship the dropdown before this test is green.scopeSubTenant{Key} relational cases to a second wave since they carry the join-ambiguity risk.users.settings JSON callbacks only once the selection semantics are settled, since saveQuietly() in a set callback bypasses model events.Maintenance. Small surface area (one trait, one manager, one Livewire component, one blade view) and MIT licensing make this fork-maintainable by a single engineer if the upstream stalls. The maintainer's funding model is premium sibling plugins, which is a mild positive on longevity but no guarantee. Budget a recurring line item at each Filament major upgrade; the plugin's dependency on panel boot() internals is the part most likely to break.
Support. No SLA, no commercial support tier for this package, GitHub issues only, and effectively no community to lean on at 30 stars. Internal ownership must be named, with a documented "how this works" note, or the knowledge concentrates in whoever ran the spike.
Scaling. The filter itself is a WHERE … IN/= on an FK — cheap, but only if the FK columns are indexed on every opted-in table; unbounded scoped queries on a large appointments table will surface that immediately. The manager caches per request, so no cross-request staleness. Scope changes trigger a full page reload, which is fine at human-interaction frequency and would be wrong if the selector ever became programmatic or automated.
Failure modes to instrument.
scopeSubTenant{Key} methods (see v1.0.4). Cover relational scopes with tests that include joins.withoutGlobalScopes() and raw query builder call sites silently bypassing scope. Lint or grep for them.Ramp-up. Low for any Filament-fluent engineer — half a day to read the trait and manager, a day to be productive. The cost is cultural, not technical: the team must internalise "is this model subtenant-scoped, and does that hold in this execution context?" as a standing review question. Ship the runbook alongside the plugin, not after the first incident.
How can I help you explore Laravel packages today?