taldres / laravel-last-seen
Track when users were last active in your Laravel application, with a middleware, query scope and facade.
Requires
- php: ^8.3
- laravel/framework: ^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.6.0
- laravel/pint: ^1.29
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.8.2|^4.0
- pestphp/pest-plugin-type-coverage: ^3.6|^4.0
- symfony/yaml: ^7.2|^8.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-09 10:12:44 UTC
README
Laravel Last Seen
A simple Laravel package to track a user's last seen and recently seen status. This package provides traits, middleware, events, and configuration to easily record and query when a user was last active in your Laravel application.
Features
- Automatically update the
last_seen_attimestamp for users - Middleware to detect user activity
- Event-based architecture for extensibility
- Query scopes and helper methods to check if a user was recently seen
- Configurable thresholds for updating and checking activity
- Migration publishing for easy setup
Requirements
PHP
PHP 8.3 or higher
Supported Laravel Versions
| Laravel Version | Package Version | PHP Version |
|---|---|---|
^12.0 |
^1.0 |
^8.3 |
^13.0 |
^1.0 |
^8.3 |
Laravel 11 and PHP 8.2 are supported up to package version 0.4.x.
Installation
-
Install the package via Composer:
composer require taldres/laravel-last-seen
-
Publish the migration and configuration files:
php artisan vendor:publish --provider="Taldres\LastSeen\LastSeenServiceProvider" -
Clear the configuration cache to ensure the new settings are loaded:
php artisan optimize:clear # or php artisan config:clear -
Run the migration to add the
last_seen_atcolumn to your users table:php artisan migrate
If your users table already has a
last_seen_atcolumn, the migration stops with an error instead of taking over a column it would drop on rollback. Delete the published migration in that case. -
Add the
Taldres\LastSeen\Trait\LastSeentrait to your User model:use Taldres\LastSeen\Trait\LastSeen; class User extends Authenticatable { use LastSeen; // ... }
-
Add the middleware to your
weborapimiddleware group inbootstrap/app.php, or to individual routes:// bootstrap/app.php ->withMiddleware(function (Middleware $middleware): void { $middleware->web(append: [ \Taldres\LastSeen\Middleware\UpdateLastSeenMiddleware::class, ]); // Token-based APIs, e.g. with Laravel Sanctum: $middleware->api(append: [ \Taldres\LastSeen\Middleware\UpdateLastSeenMiddleware::class, ]); })
The middleware resolves the authenticated user after the request has been handled, so it also works when authentication happens later in the stack, e.g. through a route-level
auth:sanctummiddleware.
Configuration
If necessary or in case of a newer version, you can publish the configuration file to customize the package settings:
php artisan vendor:publish --provider="Taldres\LastSeen\LastSeenServiceProvider" --tag="config"
or this command to force overwrite the existing configuration file:
php artisan vendor:publish --provider="Taldres\LastSeen\LastSeenServiceProvider" --tag="config" --force
In the config/last-seen.php file, you can specify the User model:
models.user: The fully qualified class name of the User model. The migration uses it to find the users table.
All other settings—such as enabling/disabling the feature, update thresholds, and recently seen thresholds—can be controlled via environment variables in your .env file:
LAST_SEEN_ENABLED: Enables or disables the package globally (default: true). It affects only theupdateLastSeenAtmethod and the middleware.LAST_SEEN_UPDATE_THRESHOLD: Minimum seconds between last_seen_at updates (default: 60)LAST_SEEN_RECENTLY_SEEN_THRESHOLD: Seconds a user is considered recently seen after last activity (default: 300)
Both thresholds must be integers of 0 or more. Other values throw an InvalidArgumentException.
Each setting has a default value, so you only need to override them if you want to change the default behavior.
Usage
How Activity Is Tracked
- Every request that passes through the middleware with an authenticated user counts as activity. This includes background requests such as polling, which can keep an otherwise idle user "recently seen". Leave the middleware off routes that should not count as activity.
- For each such request of a tracked user the middleware fires a
UserWasActiveEvent. The event means "activity detected", not "timestamp written":last_seen_atis only written onceLAST_SEEN_UPDATE_THRESHOLDseconds have passed since the stored value, so it can lag behind the latest activity by up to that many seconds. Parallel requests write it only once. - The middleware records the user that is authenticated once the request has been handled: the user of the default
guard, or of the guard an
auth:<guard>middleware selected. Logout requests are therefore not recorded, and when a request switches users, for example while an admin impersonates someone, only the final user is. - Error responses count as activity too. Registering the middleware twice, for example globally and on a route, still records each request once.
- If resolving the user or recording fails, for example because of a database error or an exception in your
trackUsing()callback or one of your listeners, the exception is reported and the response is returned unchanged.
Checking Activity
$user->recentlySeen(): Returnstrueiflast_seen_atis at mostLAST_SEEN_RECENTLY_SEEN_THRESHOLDseconds ago.User::onlyRecentlySeen(): Query scope to get only recently seen users, using the same rule asrecentlySeen().
A last_seen_at in the future, e.g. caused by clock drift between servers, counts as recently seen until the next
recorded activity replaces it.
Keep app.timezone on UTC. With another timezone, last_seen_at is stored as local time, which is ambiguous for an hour
when daylight saving time ends, just like Laravel's own timestamps.
Updating Activity
$user->updateLastSeenAt(): Writeslast_seen_atif the user is tracked and the configured update threshold has passed, and returns whether the stored timestamp changed. With an update threshold of0it writes on every call unlesslast_seen_atalready holds the current time, for example within the same second with the default date format. Onlylast_seen_atis written: no model events are fired, the model'supdated_attimestamp is left untouched and other unsaved changes on the model are not persisted.$user->forgetLastSeenAt(): Setslast_seen_attonull, again without touchingupdated_ator other unsaved changes. It works even when the package is disabled.
last_seen_at is not added to your model's $fillable. If you need to mass-assign it, add it there yourself or use forceFill().
Facade
The LastSeen facade offers the same operations for code outside the model, such as controllers, jobs and commands:
use Taldres\LastSeen\Facades\LastSeen; LastSeen::record($user); // same as $user->updateLastSeenAt() LastSeen::forget($user); // same as $user->forgetLastSeenAt() LastSeen::recentlySeen($user); // same as $user->recentlySeen() LastSeen::recentlySeenSince(); // earliest last_seen_at that still counts as recently seen LastSeen::shouldTrack($user); // whether last_seen_at may be written for this user LastSeen::trackUsing($callback); // decide per user whether to track, see Privacy
The facade is also registered as the global alias LastSeen. It resolves Taldres\LastSeen\LastSeenManager from
the container, so you can also inject the manager directly. The trait, the middleware and the event listener all use it, so the same rules apply everywhere.
Events
The package fires a UserWasActiveEvent whenever activity of a tracked user is detected. You can listen to this event
for custom logic. Recording the activity never fires the event again.
Testing Your Application
LastSeen::fake() replaces the manager with a fake for the rest of the test. The middleware, the event listener and
the trait then write nothing, and you can assert which users were recorded or forgotten:
use Taldres\LastSeen\Facades\LastSeen; LastSeen::fake(); $this->actingAs($user)->get('/dashboard'); LastSeen::assertRecorded($user); LastSeen::assertNotRecorded($otherUser);
The fake offers assertRecorded(), assertRecordedTimes(), assertNotRecorded(), assertNothingRecorded(),
assertForgotten(), assertNotForgotten() and assertNothingForgotten(). Instead of a model, the assertions that
take a user also accept a callback that receives the model, for example
LastSeen::assertRecorded(fn (User $recorded) => $recorded->is($user)). LastSeen::recorded() and
LastSeen::forgotten() return the matching users. The fake keeps the configuration and your trackUsing() callback, so
users that would not be tracked are not recorded. It ignores the update threshold, so every call counts.
- Use
Event::fake([UserWasActiveEvent::class])to assert that activity was detected without writing anything. - Disable tracking with
config(['last-seen.enabled' => false])orLastSeen::trackUsing(fn () => false). - When mocking the facade, use
LastSeen::partialMock()or stubshouldTrack(). ALastSeen::spy()returnsfalsefromshouldTrack(), so the middleware never callsrecord().
Manually Dispatching the Event
You can also dispatch the UserWasActiveEvent from your own application code:
use Taldres\LastSeen\Events\UserWasActiveEvent; use Illuminate\Support\Facades\Event; Event::dispatch(new UserWasActiveEvent($user));
AI Agents
The package ships two Laravel Boost skills for coding agents:
laravel-last-seen-developmentteaches agents how to use the package.upgrade-laravel-last-seen-v1walks agents through the upgrade from 0.4 to 1.0.
Boost offers to install them when you run php artisan boost:install, or php artisan boost:update --discover in an
existing setup.
Privacy
last_seen_at is tied to a user, so it is personal data. Your application decides the purpose, the legal basis, how
users are informed and how long the data is kept. The package cannot make an application compliant on its own, but it
keeps the data small and under your control:
- It stores a single
last_seen_atcolumn on your users table. Each write overwrites the previous value. There is no activity history, and no IP address or user agent is stored. - It does not send any data to external services.
- Who may see another user's activity status is up to your application, for example through policies.
Opting Users Out
Register a callback in a service provider to decide per user whether activity is tracked, for example based on a user setting. The package does not expect any particular column for this:
use App\Models\User; use Taldres\LastSeen\Facades\LastSeen; public function boot(): void { LastSeen::trackUsing(fn (User $user): bool => ! $user->hide_activity_status); }
For users the callback rejects, the middleware fires no UserWasActiveEvent and updateLastSeenAt() leaves
last_seen_at untouched. The callback receives every model that uses the trait, so type-hint Model if more than one
of your models does. Register it once while booting: it stays active for all later requests, also in Octane or queue
workers.
Deleting the Timestamp
$user->forgetLastSeenAt()setslast_seen_attonull. Combine it with an opt-out, otherwise the next request writes it again. It throws aLogicExceptionfor a model that was loaded without its primary key.- When a user is deleted,
last_seen_atis deleted with the row. LAST_SEEN_ENABLED=falseonly stops new writes. It does not delete stored values.
Upgrading
See UPGRADE.md for the changes between major versions, including the upgrade from 0.4 to 1.0. It also
explains how to let a coding agent do the upgrade with the upgrade-laravel-last-seen-v1 skill.
Contributing
See CONTRIBUTING. Please report security vulnerabilities as described in SECURITY instead of opening a public issue.
License
MIT
