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/Policyinside channel classes — never duplicate authorization logic. - Return arrays from presence
join: returningtruesilently breaks presence membership metadata. - Keep join payloads minimal: large arrays on
pusher:member_addeddegrade performance for all subscribers. - Cache short-lived auth results: reduces DB pressure during reconnect storms without meaningful security trade-offs.
- Use typed channel classes:
PrivateChannelandPresenceChannelmake intent explicit and prevent accidental public exposure.