Skip to content

Laravel Broadcasting with Reverb: Per-Channel Authorization and Presence Channels at Scale

Go beyond basic event broadcasting. Learn how to lock down private and presence channels with fine-grained authorization, manage large presence sets efficiently, and avoid the common pitfalls that surface in production Reverb deployments.

Published · Updated · 3 min read

Laravel Broadcasting with Reverb: Per-Channel Authorization and Presence Channels at Scale

Why Channel Authorization Deserves More Attention

Most tutorials stop at Auth::check() inside a channel route. In production, that is rarely enough. A multi-tenant SaaS, a collaborative document editor, or a live support dashboard each needs per-resource authorization that mirrors the same rules enforced in your HTTP layer — not a weaker copy of them.

Laravel Reverb is a first-party WebSocket server, but the authorization model is still driven by routes/channels.php and the BroadcastServiceProvider. Getting that layer right is the real work.

Structuring Channel Routes for Real Authorization

Avoid anonymous closures for anything non-trivial. Register a dedicated channel class instead:

php artisan make:channel OrderChannel
// routes/channels.php
Broadcast::channel('orders.{orderId}', OrderChannel::class);
// app/Broadcasting/OrderChannel.php
final class OrderChannel
{
    public function join(User $user, int $orderId): bool|array
    {
        $order = Order::findOrFail($orderId);

        // Reuse the same policy you use in controllers.
        return $user->can('view', $order);
    }
}

Using $user->can() delegates to your existing OrderPolicy::view() method. One rule, two enforcement points — no drift.

Presence Channels: The join Return Value Matters

For presence channels, returning true is not enough. You must return an array; that array becomes the member metadata broadcast to all subscribers.

public function join(User $user, string $roomId): bool|array
{
    $room = ChatRoom::findOrFail($roomId);

    if (! $user->can('join', $room)) {
        return false;
    }

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

Keep this payload small. Every subscriber receives it on pusher:member_added. Sending eager-loaded relationships here is a common mistake that bloats payloads and slows join acknowledgement.

Scaling Presence Sets

Reverb stores presence membership in Redis by default when you configure the reverb driver with a Redis connection. The key concern at scale is join/leave storms — a deploy or network blip causes hundreds of clients to reconnect simultaneously.

Mitigate this with exponential back-off on the client side (Laravel Echo supports this via the authEndpoint retry config) and by keeping your auth endpoint fast:

// config/broadcasting.php — tune the Reverb connection pool
'reverb' => [
    'driver' => 'reverb',
    'key'    => env('REVERB_APP_KEY'),
    'secret' => env('REVERB_APP_SECRET'),
    'app_id' => env('REVERB_APP_ID'),
    'options' => [
        'host'   => env('REVERB_HOST', '0.0.0.0'),
        'port'   => env('REVERB_PORT', 8080),
        'scheme' => env('REVERB_SCHEME', 'http'),
    ],
],

Cache the authorization result for short-lived presence joins (5–10 seconds is safe) using a tagged cache keyed on user:{id}:channel:{name}:

public function join(User $user, string $roomId): bool|array
{
    return Cache::tags(['channel-auth'])
        ->remember("user:{$user->id}:room:{$roomId}", 8, function () use ($user, $roomId) {
            $room = ChatRoom::findOrFail($roomId);
            if (! $user->can('join', $room)) {
                return false;
            }
            return ['id' => $user->id, 'name' => $user->display_name];
        });
}

Invalidate the tag when room membership rules change (e.g., a user is removed from a room).

Broadcasting Events Only to Authorized Channels

Use broadcastOn to return a typed channel, not a raw string:

final class OrderStatusUpdated implements ShouldBroadcast
{
    public function __construct(private readonly Order $order) {}

    public function broadcastOn(): array
    {
        return [new PrivateChannel("orders.{$this->order->id}")];
    }

    public function broadcastWith(): array
    {
        return ['status' => $this->order->status->value];
    }
}

Using PrivateChannel (or PresenceChannel) ensures Reverb enforces the auth handshake before delivering the event.

Key Takeaways

  • Delegate to policies: reuse Gate/Policy inside channel classes — never duplicate authorization logic.
  • Return arrays from presence join: returning true silently breaks presence membership metadata.
  • Keep join payloads minimal: large arrays on pusher:member_added degrade performance for all subscribers.
  • Cache short-lived auth results: reduces DB pressure during reconnect storms without meaningful security trade-offs.
  • Use typed channel classes: PrivateChannel and PresenceChannel make intent explicit and prevent accidental public exposure.

Frequently asked questions

Can I reuse Laravel policies inside channel authorization classes?

Yes. Call `$user->can('action', $model)` inside your channel's `join` method. It delegates to the same Gate and Policy you use in controllers, keeping authorization logic in one place.

What happens if a presence channel's `join` method returns `true` instead of an array?

Returning `true` grants access but sends no member metadata. Other subscribers won't receive meaningful data in `pusher:member_added` events, breaking any UI that displays who is online.

How do I handle reconnect storms in Reverb with many presence channel subscribers?

Cache the authorization result for a few seconds per user/channel pair using a tagged cache. This reduces database load during mass reconnects without weakening security, since the window is too short to be exploitable in practice.

Mohamed Said

About the author

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 GitHub LinkedIn WhatsApp Email Address My CV

Have a technical challenge?

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

Discuss your project