packstub/filament-account-switcher
Filament Account Switcher adds a fast account or tenant switcher to Filament panels, letting users jump between multiple accounts or organizations without logging out. Simple setup, configurable, and built for Laravel apps.
Know what you're adopting. 45 stars, zero dependents, small-team release cadence. This is a "read the source before you ship it" dependency, not a composer require and forget. Budget half a day to read src/ — the customer docs live in the repo's docs/ folder, not in the dist archive (v4.0.4 export-ignores the changelog and dev files), so once installed you'll be reading code, not markdown. The current release line is 4.2.1; treat the changelog on GitHub as the source of truth, since only the latest set of notes ships alongside the version you install.
Install and wire it up:
composer require packstub/filament-account-switcher:^4.2
php artisan vendor:publish --tag="filament-account-switcher-config"
Register the plugin in the panel you actually want it in — not in a shared base provider:
public function panel(Panel $panel): Panel
{
return $panel
->plugin(AccountSwitcherPlugin::make())
// ...
}
Check the exact FQCN and publish tags in src/ and the package's service provider; the namespace follows Packstub\FilamentAccountSwitcher\....
Where to look first, in order:
config/filament-account-switcher.php — every behaviour toggle (dev logins on/off, linked-account resolution, password confirmation) should be here. If a behaviour isn't configurable, note it now; that's a fork candidate.docs/ on GitHub for the linked-accounts data model and the search-resolver examples.First use case — impersonation in a support workflow. Add the impersonate action to your Users resource so support can reproduce a bug as the reporting customer:
Tables\Actions\Action::make('impersonate')
->visible(fn (User $record) => auth()->user()->can('impersonate', $record))
->action(fn (User $record) => /* the package's switch action */),
That single flow is the 80% case. Get it working, verify "stop impersonating" returns you to the original admin, then move on to linked sub-accounts.
Gate everything through policies, not the plugin config. The package decides how to switch; your app decides who may. Write an impersonate policy method and reuse it in every entry point (table action, user menu visibility, API). Rules that pay for themselves: nobody impersonates a super-admin, nobody impersonates outside their tenant, support staff only impersonate non-staff.
public function impersonate(User $user, User $target): bool
{
return $user->isStaff()
&& ! $target->isStaff()
&& $user->tenant_id === $target->tenant_id;
}
Linked sub-accounts = your ownership graph, not theirs. An agency-switches-to-client setup is a parent_id / pivot on your users (or a separate accounts model) that the package resolves. Define that relationship explicitly and constrain it in config; don't let "linked" mean "any row in the users table." The password-confirmed escalation exists precisely because switching into a sub-account is a privilege jump — keep password.confirm semantics intact rather than disabling the confirmation for convenience.
Own the search resolver — it's your scoping hook. The dev-login list is searchable, and the docs (4.2.1) show a whereLike(..., caseSensitive: false) resolver pattern. That resolver is where you enforce "only my test accounts appear": filter by a flag column, an email-domain allowlist, or a dedicated dev_logins table rather than dumping the users table into a search box. Prefer the portable whereLike form — see the gotcha on ilike below.
// resolver, roughly
User::query()
->where('is_dev_login', true)
->whereLike('email', "%{$term}%", caseSensitive: false);
Dev logins belong behind a hard gate. Wrap the dev-login capability in app()->isLocal() || app()->environment('staging') and a config flag, so a misconfigured production .env can't expose it:
'dev_logins' => env('APP_ENV') !== 'production' && (bool) env('ACCOUNT_SWITCHER_DEV_LOGINS', false),
Tenant panels need v4.0.4 or later. Version 4.0.4 fixed Missing required parameter … [Missing parameter: tenant] on tenant-less pages (/app/new, tenant registration) where the switcher built a {tenant}-scoped URL. That fix is in every 4.2.x release; if you're upgrading from 3.x or a pre-4.0.4 pin, re-test registration and any route outside a tenant context after the bump.
Audit the switch, not just the login. Impersonation is the classic "who did this?" gap. Hook the switch (model event, middleware, or a package extension point) and write an audit row: actor, target, timestamp, IP. When a customer asks why their data changed, "support impersonated them" is the answer you need on file.
Test it as a first-class flow. Pest/PHPUnit coverage for: admin can impersonate a permitted user; admin cannot impersonate another admin; stop-impersonation restores the original session user; a linked sub-account requires password confirmation; the switcher is absent for non-staff; the dev-login resolver never returns a user flagged as staff. Session state is the thing to assert — impersonation usually lives in the session, so assertSessionHas(...) plus a follow-up request is the honest test.
Pin the version and expect to fork. With 45 stars and no dependents, upgrade breakage is yours to absorb. Use an exact constraint ("4.2.1") or a caret you're prepared to test on every bump, and skim CHANGELOG.md/UPGRADE.md on GitHub before updating — they're not in vendor/. Note the jump from the previously documented 4.0.4 to 4.2.1 skipped at least one minor line; read the 4.1/4.2 entries too, not just the 4.2.1 notes.
Copying the old search-resolver docs will break on MySQL/SQLite. Pre-4.2.1 examples used ilike, which is PostgreSQL-only. If you lifted a resolver out of an older docs revision, switch it to whereLike(..., caseSensitive: false). Case-insensitivity now comes from the driver-aware builder helper, so behaviour is portable — but an ilike call that "worked" on your PG staging box will throw on a SQLite test suite.
Re-publish views if you customized the dev-login menu. 4.2.1 moved the search box behind a header icon with Escape-to-clear behaviour. If you published the package views to reword the menu (which you should, rather than subclassing), your stale copies will still render the old always-visible input. Re-publish the view tag after upgrading and re-apply your copy edits.
The search box auto-opens while a term is active — don't write a UI test that asserts the input is hidden after typing, and don't "fix" the open state with your own JS. Escape clears and closes; there is no config toggle for that behaviour, only for the list contents.
Session-based impersonation has edges. It won't survive a session driver change across subdomains, it interacts badly with forced Auth::logout() on password change, and queued jobs dispatched while impersonating will run as the impersonated user. If you dispatch mail or jobs during a support session, capture the real actor's ID first.
Notifications and mail go to the impersonated account. Support impersonating a customer will fire welcome emails, password resets, or Slack pings as that customer. In local/staging, set the mailer to log; consider a global "impersonating" check that rewrites the mail recipient during support sessions.
Don't hide the switcher with CSS. Use visible() / hidden() on the menu items and actions, driven by the same policy. A visually hidden menu item is still a live route if the package registers one — the collapsed search box in 4.2.1 is a UI affordance, not a permission boundary.
After any config edit, run php artisan config:cache — the dev-login gate is the one that will silently stay open (or closed) if you forget.
Debugging checklist: item missing → check the user menu render hook and Filament::getCurrentPanel(); search icon doesn't appear → stale published views, re-publish; switch fails silently → check the policy's before() returning false early; tenant errors → confirm you're on 4.0.4+; escalation prompts twice → your password.confirm middleware and the package's confirmation are both firing; resolver throws on a non-PG database → you're on a leftover ilike.
Judge maintenance by the repo hygiene, not the stars. The CI package-health workflow (leanness checks, SHA-pinned actions, Dependabot cooldowns, security policy) is a genuinely good signal for a package this size, and docs fixes shipping alongside feature work is a healthy sign. The flip side is a one-maintainer bus factor — read the MIT license terms and keep a fork path open before this becomes load-bearing in production.
How can I help you explore Laravel packages today?