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 Users Laravel Package

tomatophp/filament-users

Filament Users gives your Filament admin panel complete user management out of the box: CRUD for users, roles and permissions, profile and password editing, avatar uploads, and auth scaffolding. Install it and start managing users in minutes.

View on GitHub
Deep Wiki
Context7

Getting Started

Confirm the ground you're standing on before composer require. This package is a UI resource layer over your own User model — it does not own auth, guards, tenancy, or identity. You need: a running FilamentPHP 3/4 panel, a conventional users table (name/email/password, plus avatar/status columns if you use them), and Spatie Permission already wired (ideally through filament-shield). If any of those are missing, fix them first; the package will install cleanly and then behave inexplicably.

Pin the security floor explicitly. Never install pre-5.0.3. That line let an account with no update permission reset another user's password, and bulk delete/roles/teams actions ignored the user policy entirely. Resolve the pin by hand rather than letting Composer pick "latest":

composer require tomatophp/filament-users:^5.0.3
composer require bezhansalleh/filament-shield
composer require stechstudio/filament-impersonate
php artisan vendor:publish --tag=filament-users-config
php artisan optimize:clear

Wire the model contracts before touching the panel. Your User needs FilamentUser (panel access), HasAvatar (if avatars are shown), and Spatie's HasRoles. The guard implied by those roles must match your panel's authGuard() — this is the #1 cause of "the role exists but nothing works."

class User extends Authenticatable implements FilamentUser, HasAvatar
{
    use HasRoles;

    public function canAccessPanel(Panel $panel): bool
    {
        return $this->hasVerifiedEmail();
    }

    public function getFilamentAvatarUrl(): ?string
    {
        return $this->avatar_url;
    }
}

Register the plugin, then immediately write a UserPolicy. v5.0.3 is policy-driven but fails open: with no policy registered, any authenticated panel user who reaches the resource can reset passwords and change roles. Registering the plugin without a policy is the single most dangerous way to install this package.

public function panel(Panel $panel): Panel
{
    return $panel
        ->plugin(FilamentUsersPlugin::make())
        ->authGuard('web');
}

Where to look first, in order.

  1. The published config — model/resource class, navigation group, feature toggles, localization.
  2. vendor/tomatophp/filament-users/src/Resources/UserResource.php — form(), table(), getPages(), action definitions, and which methods are protected/overridable.
  3. vendor/tomatophp/filament-users/src/FilamentUsersPlugin.php — what the panel registration actually accepts.
  4. Your own subclass of the resource — this is where you will live day to day.
  5. The v5.0.3 release notes and its new tests — they encode the authorization semantics you must preserve. Read the tests; they're the real spec.

First use case — the full happy path, then the negatives. Log in, confirm the Users resource appears, create a user, assign a role, edit a profile field, reset a password, then impersonate a limited account and leave impersonation. Then deliberately try to break it: a view-only user must not see the password-reset action, and a user without delete must not be able to bulk-delete. If either succeeds, stop and fix the policy before shipping.

Implementation Patterns

Subclass, never edit vendor. Override form(), table(), getEloquentQuery(), getPages(), and action definitions in your own class, then point the config at it. This is the difference between a survivable composer update and a permanent fork you're afraid to touch.

class UserResource extends \TomatoPHP\FilamentUsers\Resources\UserResource
{
    public static function getEloquentQuery(): Builder
    {
        return parent::getEloquentQuery()
            ->with('roles')
            ->when(tenant(), fn ($q) => $q->where('tenant_id', tenant()->id));
    }
}

Policy-first, always — and remember custom actions inherit nothing. v5.0.3 filters the package's actions. Anything you add is your responsibility. Pair visibility with a real gate check inside the closure, so hiding the button and enforcing the action are the same decision:

Action::make('resetPassword')
    ->visible(fn (User $record) => auth()->user()->can('update', $record))
    ->action(function (User $record) {
        Gate::authorize('update', $record);
        // reset logic
    });

Shape the table to the support workflow, not to the schema. The highest-value day-to-day edits are here: a role filter, a trashed filter if you use SoftDeletes, a searchable email, and a verified/status badge. Keep getEloquentQuery() eager-loading roles — a role column on a 200-row page without ->with('roles') is a classic N+1 you'll only notice in production.

->filters([
    SelectFilter::make('roles')->relationship('roles', 'name')->multiple(),
    TrashedFilter::make(),
])

Ship the whole Shield workflow, not just the install. The sequence that actually works, run every time roles or permissions change:

php artisan shield:install
php artisan shield:generate --all
php artisan permission:cache-reset

The package's bulk roles/teams actions now skip records the policy disallows — but they cannot infer business rules like "don't strip the last administrator." Enforce that in a policy, a model guard, or an observer. Not in the UI.

Treat impersonation as a privileged capability. Define a dedicated ability, exclude self and super-admins, and audit the event. v5.0.3's fix covers password reset, bulk delete, roles, and teams — not impersonation. Verify its gate yourself.

Gate::define('impersonate', fn (User $actor, User $target) =>
    $actor->can('update', $target)
    && $actor->id !== $target->id
    && ! $target->hasRole('super_admin'));

Handle password hashing in exactly one place. If User casts password as hashed, do not call Hash::make() in the form. If it doesn't, hash in dehydrateStateUsing. Either way: required with confirmation on create, optional on edit, and validate against Password::defaults().

TextInput::make('password')
    ->password()
    ->dehydrated(fn ($state) => filled($state))
    ->dehydrateStateUsing(fn ($state) => Hash::make($state))
    ->required(fn (string $operation) => $operation === 'create')
    ->rule(Password::defaults());

Multi-panel and multi-tenant. Register the plugin per panel and give each a distinct navigation group, slug, or resource subclass — otherwise route names collide. For tenancy, scope getEloquentQuery() and make sure the create/edit form assigns the active tenant; the package has no tenant awareness at all.

Test both policy-present and policy-absent behavior. The fail-open case is real product behavior in v5.0.3; encode it in a test so a future upgrade can't silently flip it. Your coverage list: resource visibility, viewAny gating, password-reset authorization, bulk delete/role/team skipping, role assignment, impersonation gates. Critically, assert disallowed records are skipped, not merely hidden.

Localization and RTL are cheap wins. Publish translations and set the panel locale/direction; the package is RTL-ready, so an Arabic or Hebrew panel gets a usable users screen with zero custom CSS. Translate the action labels too — the default English "Impersonate" leaking into an RTL panel is noticeable.

Upgrade cadence. Pin a minor line, watch GitHub releases, and check Filament/Shield compatibility before every bump. Keep one smoke test that logs in as a limited admin and asserts they cannot reset another user's password or escalate their own roles. That test is your upgrade insurance.

Gotchas and Tips

Fail-open with no policy is the headline hazard. In v5.0.3, apps without a UserPolicy keep full access by design. Put Gate::getPolicyFor(User::class) in a CI assertion — if it returns null, you have an open privilege-escalation window. Write the policy covering viewAny, view, create, update, delete, restore, forceDelete, and impersonate.

If you ever ran pre-5.0.3, upgrade and audit. Hiding the action in the UI was never protection. Inventory deployed versions, advance the pin immediately, and review password-reset and role-change activity in your logs for the affected window.

Route and namespace collisions with your existing UserResource. This is the most common integration failure: Filament auto-discovers both the package resource and your old one, producing duplicate nav entries and route-name conflicts. Delete or rename the old resource and confirm via php artisan route:list exactly which one is loaded.

Guard mismatch between Filament and Spatie. Roles are guard-scoped. If the panel uses web but permissions were seeded under a different guard, hasRole() returns false while the role is plainly visible in the database. Align config/auth.php, the panel's authGuard(), and config/permission.php, then php artisan permission:cache-reset.

Double hashing. The most common self-inflicted bug: a hashed cast plus Hash::make() in the form produces a password that can never be verified. Pick one location and comment it.

Avatar column mismatches. Filament's HasAvatar contract and this package may expect different columns (avatar_url vs profile_photo_path). Map with an accessor rather than migrating the schema. Then verify storage:link, the disk, and file visibility — and queue large uploads.

Soft deletes need explicit policy coverage. Add restore and forceDelete to the policy and test the trashed filter. Bulk delete may soft-delete when the operator expected a hard delete — label the action explicitly ("Archive" vs "Delete permanently") so nobody finds out the hard way.

Self-delete and last-admin protection are your job. Policies authorize actions on other records but rarely stop a user deleting themselves or stripping their own final admin role. Add a visibility guard plus a domain-level check in an observer:

->visible(fn (User $record) => $record->id !== auth()->id())

Custom bulk actions get no filtering for free. The package's own bulk actions are filtered in v5.0.3; yours are not. Authorize each record inside the closure or filter the collection before acting. A hidden button is not enforcement.

Impersonation sits outside the v5.0.3 fix. Independently confirm it's gated, audited (impersonator ID, target ID, timestamp), blocked for super-admins, and that "leave impersonation" survives your panel middleware. Check sessions can't be replayed after logout.

Cache and config quirks. After publishing config or editing panel providers, run php artisan optimize:clear (and filament:optimize-clear where available). Only re-cache in production after confirming policy and guard settings. Stale Spatie permission caches produce false negatives that look like code bugs and waste hours.

Resource not appearing? Check in this order: plugin registered on the right panel → policy viewAny returns true → canAccessPanel() returns true → navigation not suppressed by config → Shield permissions generated → php artisan route:list shows the resource routes. Working the list in order beats guessing.

Release metadata anomaly. The latest release is stamped 2026-09-14 — verify tag provenance on GitHub/Packagist before pinning in a regulated environment. Treat it as a data-quality flag, not a reason to skip the 5.0.3 floor.

Maintenance risk is real and should shape your code. Small, single-maintainer package with low adoption. Keep it forkable: no deep vendor edits, all customization in a subclass, and a stated patch SLA. Scope it as a commodity-CRUD accelerator — it removes the screens, not your identity strategy.

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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator