Laravel Reverb Presence Channels &amp; Typed Events | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com/public) - [Home](https://www.msaied.com/public)
- [Projects](https://www.msaied.com/public/projects)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [About](https://www.msaied.com/public#about)

           [  Contact](https://www.msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com/public)
- [ProjectsCase studies](https://www.msaied.com/public/projects)
- [ArticlesEngineering notes](https://www.msaied.com/public/articles)
- [CertificatesCredentials](https://www.msaied.com/public/certificates)
- [AboutHow I work](https://www.msaied.com/public#about)
- [ContactGet in touch](https://www.msaied.com/public#contact)

  [Start a conversation](https://www.msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/articles)
4. /
5. Laravel Reverb: Building Presence Channels with Per-User State and Typed Events

 Laravel Reverb: Building Presence Channels with Per-User State and Typed Events
================================================================================

 Presence channels in Laravel Reverb go far beyond simple pub/sub. Learn how to track per-user state, broadcast typed events, and keep channel membership consistent under reconnects and horizontal scale.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 3 Jul 2026 · Updated 3 Jul 2026 · 3 min read

ShareCopy linkCopied

 ![Laravel Reverb: Building Presence Channels with Per-User State and Typed Events](https://cdn.msaied.com/352/9b3c490b8303fdc84442671965a3ee8a.png) 

  On this page +1. [Presence Channels in Laravel Reverb: Per-User State and Typed Events](#presence-channels-in-laravel-reverb-per-user-state-and-typed-events)
2. [Authenticating and Enriching Channel Membership](#authenticating-and-enriching-channel-membership)
3. [Typed Broadcast Events](#typed-broadcast-events)
4. [Handling Reconnects Without Ghost Members](#handling-reconnects-without-ghost-members)
5. [Scaling Across Multiple Reverb Workers](#scaling-across-multiple-reverb-workers)
6. [Key Takeaways](#key-takeaways)

 Presence Channels in Laravel Reverb: Per-User State and Typed Events
--------------------------------------------------------------------

Most Reverb tutorials stop at public and private channels. Presence channels are where real collaborative features live — shared cursors, live user lists, typing indicators — and they demand a more disciplined approach to state, typing, and reconnection.

### Authenticating and Enriching Channel Membership

Presence channels authenticate via `BroadcastServiceProvider` just like private channels, but the return value matters: whatever you return from the closure becomes the member's metadata broadcast to every subscriber.

```php
// routes/channels.php
Broadcast::channel('document.{documentId}', function (User $user, int $documentId): array|false {
    $document = Document::find($documentId);

    if (! $document || ! $user->can('view', $document)) {
        return false;
    }

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

```

Reverb stores this payload in its internal channel registry (backed by Redis in multi-worker deployments). Every `here`, `joining`, and `leaving` event on the client receives this shape — so keep it lean and serialisable.

### Typed Broadcast Events

Avoid stringly-typed payloads. Define a dedicated event class per domain action:

```php
final class CursorMoved implements ShouldBroadcastNow
{
    use Dispatchable, InteractsWithSockets;

    public function __construct(
        public readonly int    $userId,
        public readonly string $documentId,
        public readonly float  $x,
        public readonly float  $y,
    ) {}

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

    public function broadcastAs(): string
    {
        return 'cursor.moved';
    }

    /** Exclude the sender from receiving their own event. */
    public function broadcastToEveryoneElse(): static
    {
        return $this->dontBroadcastToCurrentUser();
    }
}

```

Using `broadcastAs()` gives you a clean contract between server and client. On the Echo side:

```javascript
window.Echo.join(`document.${documentId}`)
    .here(members  => store.setMembers(members))
    .joining(member => store.addMember(member))
    .leaving(member => store.removeMember(member))
    .listen('.cursor.moved', ({ userId, x, y }) => {
        cursors.update(userId, x, y);
    });

```

Note the leading dot in `.cursor.moved` — required when you override `broadcastAs()`.

### Handling Reconnects Without Ghost Members

When a client drops and reconnects, Reverb fires `leaving` then `joining` in sequence. If your server-side state (e.g., a Redis hash of active collaborators) is only updated via those events, a crash loop creates ghost entries.

Mitigate this with a heartbeat approach:

```php
// In a Livewire component or dedicated endpoint
public function heartbeat(string $documentId): void
{
    $key = "presence:document:{$documentId}:user:{$this->userId}";
    Redis::setex($key, 30, now()->timestamp);
}

```

A scheduled command (every minute) prunes keys older than the TTL, keeping your membership list consistent independently of WebSocket events.

### Scaling Across Multiple Reverb Workers

Reverb uses Redis pub/sub to synchronise presence state across workers. Ensure your `config/reverb.php` points to a dedicated Redis connection — do not share it with your queue connection:

```php
'servers' => [
    'reverb' => [
        'driver'     => 'reverb',
        'connection' => env('REVERB_REDIS_CONNECTION', 'reverb'),
    ],
],

```

```php
// config/database.php  (redis connections)
'reverb' => [
    'url'      => env('REVERB_REDIS_URL'),
    'host'     => env('REVERB_REDIS_HOST', '127.0.0.1'),
    'password' => env('REVERB_REDIS_PASSWORD'),
    'port'     => env('REVERB_REDIS_PORT', '6379'),
    'database' => env('REVERB_REDIS_DB', '1'),
],

```

Isolating the connection prevents queue-heavy workloads from starving the pub/sub pipeline.

### Key Takeaways

- Return a structured array from channel auth closures — it becomes the member metadata shape for all clients.
- Use dedicated event classes with `broadcastAs()` for a typed, versioned contract between PHP and JavaScript.
- Call `dontBroadcastToCurrentUser()` on high-frequency events (cursors, keystrokes) to halve unnecessary traffic.
- Supplement WebSocket membership events with a Redis TTL heartbeat to survive reconnect storms.
- Isolate Reverb's Redis connection from your queue connection to prevent throughput contention at scale.

- [laravel](https://www.msaied.com/public/articles?search=laravel)
- [reverb](https://www.msaied.com/public/articles?search=reverb)
- [websockets](https://www.msaied.com/public/articles?search=websockets)
- [broadcasting](https://www.msaied.com/public/articles?search=broadcasting)

 Frequently asked questions 
---------------------------

  Does Laravel Reverb support presence channels out of the box, or do I need extra packages?Reverb supports presence channels natively via Laravel Echo and the standard `PresenceChannel` class. No additional packages are required beyond `laravel/reverb` and `laravel-echo`.

   Why use `ShouldBroadcastNow` instead of `ShouldBroadcast` for cursor events?`ShouldBroadcastNow` bypasses the queue and broadcasts synchronously, which is critical for low-latency events like cursor movements. Queued broadcasting adds unpredictable delay that makes real-time UI feel broken.

   How do I prevent a user's own cursor event from echoing back to them?Call `$this-&gt;dontBroadcastToCurrentUser()` inside the event or expose a `broadcastToEveryoneElse()` helper that returns it. Reverb will exclude the socket that triggered the broadcast from receiving the event.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://www.msaied.com/public#person)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

[About](https://www.msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleLaravel Caching Strategies: Tags, Stampede Prevention, and Cache-Aside at Scale](https://www.msaied.com/public/articles/laravel-caching-strategies-tags-stampede-prevention-and-cache-aside-at-scale-1) [Next articleJob Batching, Chaining, and Rate-Limited Middleware in Laravel Queues](https://www.msaied.com/public/articles/job-batching-chaining-and-rate-limited-middleware-in-laravel-queues-2)  

   On this page
-------------

1. [Presence Channels in Laravel Reverb: Per-User State and Typed Events](#presence-channels-in-laravel-reverb-per-user-state-and-typed-events)
2. [Authenticating and Enriching Channel Membership](#authenticating-and-enriching-channel-membership)
3. [Typed Broadcast Events](#typed-broadcast-events)
4. [Handling Reconnects Without Ghost Members](#handling-reconnects-without-ghost-members)
5. [Scaling Across Multiple Reverb Workers](#scaling-across-multiple-reverb-workers)
6. [Key Takeaways](#key-takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/public/sitemap.xml)
