Laravel Magazine
Building a "Who's Online" Presence Tracker with Laravel Reverb

Building a "Who's Online" Presence Tracker with Laravel Reverb

Eric Van Johnson ·

Public broadcast channels tell you an event happened. Private channels tell you a specific user can listen. Presence channels do something different: they track exactly who is currently connected, and notify everyone else the moment someone joins or leaves. This tutorial builds a live "who's online" list for a shared document view using Laravel Reverb.

Step 1: Install and configure Reverb

php artisan install:broadcasting

This scaffolds Reverb, sets your BROADCAST_CONNECTION to reverb in .env, and wires up the frontend Echo configuration for you.

Step 2: Define the presence channel

Presence channels use the same channels.php file as private channels, but the authorization callback returns an array of data about the user rather than a plain boolean — that returned array is what makes someone "present."

use App\Models\Document;
use Illuminate\Support\Facades\Broadcast;

Broadcast::channel('document.{documentId}', function ($user, $documentId) {
    $document = Document::findOrFail($documentId);

    if (! $user->canView($document)) {
        return null;
    }

    return [
        'id' => $user->id,
        'name' => $user->name,
        'avatar' => $user->avatar_url,
    ];
});

Returning null denies access, same as returning false would on a private channel. Returning an array both authorizes the connection and defines what data other members of the channel will see about this person.

Step 3: Join the presence channel from the frontend

import Echo from 'laravel-echo';

const channel = window.Echo.join(`document.${documentId}`)
    .here((users) => {
        // Everyone currently present, on initial join
        setOnlineUsers(users);
    })
    .joining((user) => {
        setOnlineUsers((current) => [...current, user]);
    })
    .leaving((user) => {
        setOnlineUsers((current) => current.filter((u) => u.id !== user.id));
    })
    .error((error) => {
        console.error('Presence channel error', error);
    });

.here() fires once, immediately, with the full roster of everyone already connected. .joining() and .leaving() fire afterward as people connect and disconnect — Reverb handles the bookkeeping of tracking connections and notifying the channel automatically, you never manually broadcast a "user joined" event yourself.

Step 4: Broadcasting activity within the channel

Presence gives you who's online; you can layer ordinary events onto the same channel for activity like cursor position or "is typing" indicators.

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class UserIsTypingInDocument implements ShouldBroadcast
{
    use InteractsWithSockets;

    public function __construct(
        public int $documentId,
        public int $userId,
    ) {}

    public function broadcastOn(): Channel
    {
        return new PresenceChannel("document.{$this->documentId}");
    }
}

Listen for it the same way as any other Echo event, on the same channel instance you already joined:

channel.listen('UserIsTypingInDocument', (event) => {
    showTypingIndicator(event.userId);
});

Step 5: Rendering the roster

<div class="flex -space-x-2">
    @foreach($onlineUsers as $user)
        <img src="{{ $user['avatar'] }}"
             title="{{ $user['name'] }}"
             class="w-8 h-8 rounded-full border-2 border-white">
    @endforeach
</div>

(This part is rendered client-side from the onlineUsers state built up by .here()/.joining()/.leaving() — the Blade above is illustrative of the shape of the data, not a server-rendered list, since presence is inherently a live client-side concept.)

A note on scaling

Presence channels require Reverb (or Pusher) to track connection state per channel, which means every server in a horizontally scaled Reverb deployment needs to share that state — Reverb's Redis scaling mode handles this for you when you run more than one Reverb instance behind a load balancer. For a single-server deployment, the default setup here works with no extra configuration.

Presence channels turn "is anyone else looking at this right now" from a polling endpoint you'd have to build and rate-limit yourself into something Reverb tracks for free.

Stay Updated

Subscribe to our newsletter

Get latest news, tutorials, community articles and podcast episodes delivered to your inbox.

Weekly articles
We send a new issue of the newsletter every week on Friday.
No spam
We'll never share your email address and you can opt out at any time.