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 Subtenant Scope Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

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.

  • Blast radius is per-model, not per-resource. You cannot pilot on one screen; opting in one resource changes every query on that model in that panel. Consistency is the upside; the downside is that adoption is not incrementally reversible without removing the trait.
  • Unscoped execution contexts. The global scope is panel-aware and no-ops outside the registered panel. Queued jobs, scheduled reports, CSV exports, API controllers and console commands see the unscoped dataset while the UI sees the scoped one. Any KPI that must reconcile between a dashboard widget and an overnight job needs deliberate handling — this is the single most likely source of a production incident.
  • URL param trust. Resolution order is URL → session → user storage, and the URL param is read on first hit. 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).
  • Single-option mode applies no filter. One accessible option renders a static label and does not scope the query. Correct in principle, but it means the same user sees filtered and unfiltered behaviour depending on how many areas are assigned — a data-exposure surprise during permission changes.
  • Global-scope fragility. v1.0.4 fixed an ambiguous-column bug when a scoped query joined a table with the same FK. That is a class of bug, not an instance; expect more from joins, whereHas, and withoutGlobalScopes() call sites.
  • Maturity. 30 stars, 0 dependents, one maintainer, first release 2026-10. The code is small enough to fork, but there is no community hardening behind it.

Key questions.

  1. Is "scope" a soft working context or a hard authorization boundary? (If the latter: stop.)
  2. Does the manager validate a URL-supplied ID against optionsQuery for the current user and tenant?
  3. How does the subtenant global scope compose with Filament's built-in tenant global scope — order, and behaviour under withoutGlobalScopes()?
  4. Which background contexts (jobs, exports, reports, API) read the same models, and what should they do?
  5. What is the UX cost of the full page reload on every scope change, and does it fire on the All option too?
  6. Who owns this internally if the maintainer stops shipping — and is the MIT fork path acceptable to us?

Integration Approach

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.

  1. Spike (1–3 days): one scope, on a single non-critical panel, against a model with no background-job consumers. Confirm the CSS build, the reload behaviour, and interaction with built-in tenancy.
  2. Authorization harness first, feature second: prove that a URL-supplied ID outside optionsQuery is rejected (or wrap the plugin's resolution to enforce it). Do not ship the dropdown before this test is green.
  3. Enumerate unscoped contexts: list every job, command, export and endpoint touching candidate models and decide explicitly — scope it manually, leave it unscoped, or block it.
  4. Model-by-model rollout behind a flag, starting with models whose FK is a plain column; defer the scopeSubTenant{Key} relational cases to a second wave since they carry the join-ambiguity risk.
  5. Persistence last: session-only to start; add the users.settings JSON callbacks only once the selection semantics are settled, since saveQuietly() in a set callback bypasses model events.

Operational Impact

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.

  • Forgotten opt-in on a new resource → mixed-scope data in a list, visible but not obviously wrong. Mitigate with a PR checklist item and a test that asserts every resource on a scoped model carries the trait.
  • Single-option silent no-filter → full dataset shown to a one-area user after a permission change. Test it.
  • Background job / export divergence from the UI. Log the active scope on any report job and reconcile totals in a smoke test.
  • Unauthorized URL param acceptance. Explicit negative test.
  • Join-ambiguity regressions in 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.
  • Purged CSS after a theme change. Include the dropdown in visual regression or at least a manual smoke step.

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.

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.
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
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode