mominalzaraa/filament-team-guard
Filament Team Guard adds team-based access control to Filament panels: assign users to teams, switch the active team, and automatically scope resources, pages, and queries so each team only sees its own data. Ideal for secure multi-tenant admin panels.
Install and publish
composer require mominalzaraa/filament-team-guard:^2.1.1
php artisan vendor:publish --tag="filament-team-guard-config"
php artisan vendor:publish --tag="filament-team-guard-views" # optional, only if you need to touch UI
Pin to ^2.1.1 rather than ^2.0 — anything below 2.1.1 has a broken customization path for team renaming (see Gotchas).
Where to look first
config/filament-team-guard.php — the binding/namespace options for the team actions and the Livewire pages.app/Providers/Filament/*PanelProvider.php) — the package registers its team pages/components here; this is where you enable/disable pieces and wire the panel into Jetstream teams.App\Actions\FilamentJetstream\* — this is the seam the package exposes for overriding behaviour. If UpdateTeamName exists there, the package treats it as your override.First use case
The common entry point is: you have a Jetstream-style teams table (teams, team_user, team_invitations) and you want Filament to manage team membership and team settings without duplicating Jetstream's UI. Get that working end to end first — a panel page that lists the current user's teams, lets them switch, and renders the team settings/name form — before touching roles or resource scoping. The rename form is the fastest smoke test that the container bindings are correct, because it round-trips through the UpdatesTeamNames contract.
Override behaviour via the contract, not by editing vendor code
The supported extension path is to bind your own implementation of Laravel\Jetstream\Contracts\UpdatesTeamNames (or the package's published action class) in a service provider:
// app/Providers/AppServiceProvider.php
use App\Actions\FilamentJetstream\UpdateTeamName;
use Laravel\Jetstream\Contracts\UpdatesTeamNames;
public function register(): void
{
$this->app->singleton(UpdatesTeamNames::class, UpdateTeamName::class);
}
Do this in register(), not boot(). boot() runs after Livewire components resolve their dependencies in some flows, and you'll get the default implementation silently winning.
Publish-then-override, don't fork
Publish the action classes once, commit them, then treat them as application code. The package's regression tests for v2.1.1 explicitly cover "default binding" and "custom binding" paths, which tells you both are first-class — you should never need to subclass the Livewire component itself just to change what happens on save.
Team-scoped resources
Pair the team guard with Filament's tenancy: point the panel's tenant() at the current Jetstream team and let the guard enforce membership. Keep role checks in policies, not in the page classes — the package pages are thin, and sprinkling ->can() in resources gets unmanageable once you have more than a couple of team-owned models.
Testing your integration
Mirror what the package does: assert the contract is bound to your class, then assert the Livewire component calls through it.
it('resolves the custom team name updater', function () {
expect(app(UpdatesTeamNames::class))->toBeInstanceOf(UpdateTeamName::class);
});
Livewire::test(UpdateTeamName::class)
->set('name', 'Renamed')
->call('updateTeamName')
->assertHasNoErrors();
expect($team->fresh()->name)->toBe('Renamed');
Use a real SQLite file or :memory: with migrations run, not the package's own isolated test config — the package's SQLite setup is scoped to its suite and won't include your Jetstream migrations.
The 2.1.1 fix matters if you customized team renaming. Before this release, the UpdateTeamName Livewire component called $team->update() directly, bypassing the UpdatesTeamNames contract entirely. If you published App\Actions\FilamentJetstream\UpdateTeamName and wondered why your validation, event dispatch, or audit logging never fired on rename — that was the cause, not your binding. Upgrade, then re-verify your override actually runs (a ray()/Log::info() in the action is the quickest proof). Reported upstream as issue #1, so expect this to have bitten others on 2.0.x.
Binding precedence is easy to get wrong. The order that matters: explicit container binding > published action class > package default. If your custom action isn't being used, dump app(UpdatesTeamNames::class)::class before assuming the package is at fault. Also clear php artisan config:clear after editing the config file's namespace keys — a cached config silently negates the publish.
Two namespaces look alike. Jetstream's own App\Actions\Jetstream\UpdateTeamName and the package's App\Actions\FilamentJetstream\UpdateTeamName are different classes with the same contract. Bind the one you actually edited; mixing them means the Filament page and Jetstream's API/profile page mutate teams through different code paths, and you'll get inconsistent behaviour that's painful to trace.
Livewire component state is not refreshed by the action. The component keeps the submitted name in its own property. If your custom UpdatesTeamNames normalizes input (trim, title-case, strip emoji), the form can show the un-normalized value until reload. Either normalize in the component before delegating or re-hydrate from $team->fresh() in the action's return path.
Don't put authorization inside the action. The contract is a pure "persist this name" seam. Authorization belongs in the component (which the package handles) or a policy — putting Gate::authorize in your action means Jetstream's non-Filament callers start failing too, since the contract is shared.
Extension points to prefer, in order: config toggles → contract bindings → published actions → published views. Reaching for the last two means you've taken on upgrade burden; the 2.1.1 fix is a good reminder that the first two are the ones with test coverage behind them.
Version pinning. Add the package to your upgrade checklist for Filament major bumps — the Jetstream contract layer is stable, but the Livewire components depend on Filament's form/page APIs, which churn.
How can I help you explore Laravel packages today?