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

Laravel Last Seen Laravel Package

taldres/laravel-last-seen

A lightweight Laravel package for tracking user last-seen timestamps and online status. Record activity, query when users were last active, and display presence indicators with minimal setup and Eloquent-friendly integration. Ideal for dashboards, admin panels, and member directories.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require taldres/laravel-last-seen
php artisan vendor:publish --provider="Taldres\LastSeen\LastSeenServiceProvider"
php artisan migrate

Where to look first:

  1. config/last-seen.php (published) — confirms the column name, the cache/throttle behaviour, and any "online" window settings. This is the single source of truth if something "doesn't update".
  2. app/Models/User.php — add the trait:
use Taldres\LastSeen\Traits\HasLastSeen;

class User extends Authenticatable
{
    use HasLastSeen;
}
  1. app/Http/Kernel.php — register the middleware in the web group (or a narrower group, see below):
protected $middlewareGroups = [
    'web' => [
        // ...
        \Taldres\LastSeen\Http\Middleware\LastSeen::class,
    ],
];
  1. database/migrations/*_add_last_seen_at_* — check the column type (timestamp nullable) matches what the trait/middleware expects.

First use case: show presence next to a user in a listing.

{{ $user->name }}
@if ($user->last_seen_at)
    <span class="text-xs text-gray-500">
        {{ $user->last_seen_at->diffForHumans() }}
    </span>
@endif

Hit any web route while authenticated, then dd(auth()->user()->fresh()->last_seen_at) — if it's still null, the problem is middleware registration or the column, not the package.

Implementation Patterns

Attach the middleware narrowly, not globally. Putting it on web means a DB write on every asset-less page view. Common alternatives:

// only where presence actually matters
Route::middleware(['auth', \Taldres\LastSeen\Http\Middleware\LastSeen::class])
    ->group(function () {
        Route::get('/dashboard', DashboardController::class);
    });

Online list / "who's active" query. Drive it from config rather than hardcoding the window:

$window = config('last-seen.online_window', 5); // minutes

$online = User::query()
    ->where('last_seen_at', '>=', now()->subMinutes($window))
    ->orderByDesc('last_seen_at')
    ->get();

API surface. Don't leak raw timestamps to the front end; ship both forms:

return [
    'last_seen_at'   => $this->last_seen_at?->toIso8601String(),
    'last_seen_human' => $this->last_seen_at?->diffForHumans(),
    'is_online'      => $this->last_seen_at?->gt(now()->subMinutes(5)) ?? false,
];

Consistent shape here means the SPA can render presence without re-implementing the window logic.

Presence badge component (Blade or Livewire) so the "online / 5m / 2d ago" decision lives in one place, keyed off the same config value the query uses. Otherwise the list and the badge will disagree at the boundary.

Testing. Freeze time and assert against the column directly:

Carbon::setTestNow('2024-01-01 12:00:00');

$this->actingAs($user)->get('/dashboard')->assertOk();

$this->assertTrue(
    $user->fresh()->last_seen_at->equalTo(now())
);

Throttling. If you keep the middleware on a busy web group, add an if (! Cache::has(...)) guard around the update, or wrap the write in Cache::lock(). Writes on every request are the main cost of this package in production.

Integration with queues / jobs. The middleware only fires on HTTP. Long-running work (websockets, queue workers, scheduled jobs) will make users look offline. If presence matters there, call the same update path explicitly from a job or a PresenceService.

Gotchas and Tips

  • Middleware ordering. It must run after StartSession and auth middleware, otherwise auth()->user() is null and nothing is written. Symptom: works on some routes, silently no-ops on others.
  • API / Sanctum routes. web group registration does nothing for api. Add the middleware to the api group or to specific route groups if mobile/SPA clients need presence.
  • Column mismatch is the #1 failure. If you already have a last_seen_at (different type, different name, or a last_activity column), publish and edit the config rather than editing the migration after it has run.
  • $fillable / mass assignment. The package's write path may bypass fillable, but any of your own code doing $user->update(['last_seen_at' => ...]) will silently drop the field. Add it to $fillable or use forceFill.
  • Timestamps. Updating last_seen_at on every request also bumps updated_at if you use a normal save(), which breaks "profile last edited" displays. Prefer a query-builder update or updateQuietly.
  • Timezone. Store UTC, render local: $user->last_seen_at->timezone(config('app.timezone'))->diffForHumans(). Comparing a naive local now() against a UTC column produces "in 2 hours" artifacts.
  • Cache driver differences. Any cache-throttling behaviour behaves differently under array (tests) vs redis (prod). Assert on the column in tests, not on cache keys.
  • diffForHumans() on null. Always null-check; new users who never hit a middleware route will have null, not a zero timestamp.
  • Debugging checklist: php artisan route:list --path=dashboard -v (is the middleware listed?) → check the column exists via Schema::hasColumn('users', 'last_seen_at') → DB::listen() to see whether an UPDATE fires at all.
  • Extension points. Publish the config to change column/window; override the trait method in your User model if you need custom "online" semantics; wrap the whole thing in your own service if you plan to move presence to Redis later — keeps the call sites stable.
  • On v1.6.2 specifically: this release is a tooling/code-style alignment with Laravel conventions. No public API or config changes are signalled, so no migration work is required. After upgrading, still run your own suite plus php artisan vendor:publish --force into a scratch dir and diff the config against yours — style-only releases occasionally reformat arrays or key ordering, and a hardcoded lookup will break silently if you copy the new file over.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource