Admin User Invites
The Filament admin panel does not let users sign up. Every user is created by an existing admin, who triggers a welcome email containing a password-setup link. This page covers the invite flow, the token-expiry settings, and the signals admins use to know whether an invited user has actually accessed the panel.
Invite flow
Section titled “Invite flow”When an admin creates a user from /admin/users:
- A random password is set on the user record (the user never sees or uses it).
- A Laravel password-reset token is generated via
Password::broker()->createToken(). - A
WelcomeUserNotificationemail is queued, containing the Filament reset URL:Filament::getPanel('admin')->getResetPasswordUrl($token, $user). - The user clicks the link, sets a password, and is auto-logged into the panel on submit (Filament’s default behaviour for the reset page).
All three steps are encapsulated in App\Services\User\UserInviteService::send(),
which is called from both the create flow and the resend action.
Token expiry
Section titled “Token expiry”Reset tokens (including the welcome-email link) expire after 5 days, configured
at passwords.users.expire = 7200 (minutes) in config/auth.php. The same setting
applies to “Forgot password” links on /admin/login.
Expired tokens stay in the password_reset_tokens table until they are replaced
(by a new invite or reset request). Laravel returns the same generic
“Invalid token” error for both expired and structurally-invalid tokens.
Resending an invite
Section titled “Resending an invite”If an invite link expires or is lost, admins can resend it from
/admin/users/{id}/edit using the Resend invite header action. The action:
- Generates a fresh token (replacing any previous row in
password_reset_tokens). - Builds a new Filament reset URL.
- Re-sends
WelcomeUserNotification.
The old invite link stops working the moment Resend is clicked, because its token hash no longer matches the new row. The user should always use the most recent email they received.
Resend is restricted to users with the admin role and is hidden on the admin’s
own user record.
Knowing whether an invited user has accessed the panel
Section titled “Knowing whether an invited user has accessed the panel”Every login bumps users.last_login_at. This is the authoritative signal for
“has this user actually accessed the app?”:
last_login_at IS NULL— the user has never logged in. If the invite was sent more than a few minutes ago, the link is probably unused and a resend may be needed.last_login_at IS NOT NULL— the user has accessed the panel at least once. The displayed value is the most recent login moment and never expires.
The Users list (/admin/users) shows this as a sortable Last login column:
a red “Never logged in” badge when null, a green relative date otherwise.
How it is recorded
Section titled “How it is recorded”App\Listeners\UpdateLastLoginAt listens to Laravel’s
Illuminate\Auth\Events\Login event and calls
forceFill(['last_login_at' => now()])->saveQuietly() on the authenticated user.
Auto-discovery picks it up — no manual registration in
AppServiceProvider is needed.
Impersonation is excluded
Section titled “Impersonation is excluded”When an admin starts impersonating a supplier via the Impersonate action,
Laravel’s Auth::login($target) also fires a Login event for the target.
To keep last_login_at an accurate signal of real user access,
UserImpersonation::start() sets the static flag
UserImpersonation::$loginIsImpersonation = true around the Auth::login()
call. The listener checks this flag and skips the bump while it is set.
The flag is cleared in a finally block so a failure inside Auth::login()
cannot leave it stuck.
What the password_reset_tokens table does not tell you
Section titled “What the password_reset_tokens table does not tell you”It is tempting to use “has a row in password_reset_tokens” as a proxy for
“invite not yet used”. This is unreliable in practice, because the same table
is also used by any user who clicks Forgot password on /admin/login.
A long-term user who once clicked Forgot Password without finishing will show
up as “pending” even though they have a working password and use the panel
daily. Always rely on last_login_at instead.
Related files
Section titled “Related files”app/Services/User/UserInviteService.php— shared invite logic.app/Filament/Resources/Users/Pages/CreateUser.php— first-time invite.app/Filament/Resources/Users/Pages/EditUser.php— hosts the Resend action in the page header.app/Filament/Resources/Users/Actions/ResendInviteAction.php— the action itself, with the admin-only visibility check.app/Notifications/WelcomeUserNotification.php— the email body.app/Listeners/UpdateLastLoginAt.php— theLogin-event listener.app/Services/UserImpersonation.php— sets the$loginIsImpersonationflag duringAuth::login($target).config/auth.php—passwords.users.expire(minutes).