Advanced Laravel Broadcasting: Real-Time Applications

· 6 min read

Laravel Broadcasting allows you to broadcast data to connected clients in real-time. Combined with Reverb (Laravel's WebSocket server), you can build chat applications, live notifications, collaborative tools, and more.

Setup

Install Reverb:

composer require laravel/reverb
php artisan reverb:install

Configure in .env:

REVERB_APP_ID=your-app-id
REVERB_APP_KEY=your-app-key
REVERB_APP_SECRET=your-app-secret
REVERB_HOST=localhost
REVERB_PORT=8080

Start Reverb:

php artisan reverb:start

Basic Broadcasting Event

Create a broadcastable event:

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithBroadcasting;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class MessageSent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithBroadcasting, SerializesModels;

    public function __construct(
        public Message $message,
    ) {}

    public function broadcastOn(): array
    {
        return [
            new Channel('chat.' . $this->message->conversation_id),
        ];
    }

    public function broadcastAs(): string
    {
        return 'message.sent';
    }

    public function broadcastWith(): array
    {
        return [
            'id' => $this->message->id,
            'text' => $this->message->text,
            'user' => $this->message->user->only(['id', 'name']),
        ];
    }
}

Listen on Frontend

// resources/js/chat.js
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';

window.Pusher = Pusher;

window.Echo = new Echo({
    broadcaster: 'reverb',
    key: import.meta.env.VITE_REVERB_APP_KEY,
    wsHost: import.meta.env.VITE_REVERB_HOST,
    wsPort: import.meta.env.VITE_REVERB_PORT,
    wssPort: import.meta.env.VITE_REVERB_PORT,
    forceTLS: import.meta.env.VITE_REVERB_SCHEME === 'https',
    enabledTransports: ['ws', 'wss'],
});

// Listen for messages
window.Echo.channel(`chat.${conversationId}`)
    .listen('message.sent', (data) => {
        console.log('New message:', data);
        addMessageToUI(data);
    });

Private Channels

Restrict access to specific users:

// routes/channels.php
use App\Models\User;
use App\Models\Conversation;

Broadcast::channel('conversation.{id}', function (User $user, int $id) {
    return $user->conversations()->where('id', $id)->exists();
});

Broadcasting to private channels:

class MessageSent implements ShouldBroadcast
{
    public function broadcastOn(): array
    {
        return [
            new PrivateChannel('conversation.' . $this->message->conversation_id),
        ];
    }
}

Listen on frontend:

window.Echo.private(`conversation.${conversationId}`)
    .listen('message.sent', (data) => {
        addMessageToUI(data);
    });

Presence Channels

Track which users are online and viewing:

// routes/channels.php
Broadcast::channel('conversation.{id}:presence', function (User $user, int $id) {
    return $user->conversations()->where('id', $id)->exists()
        ? ['id' => $user->id, 'name' => $user->name]
        : false;
});

Broadcasting to presence channel:

class UserTyping implements ShouldBroadcast
{
    public function broadcastOn(): array
    {
        return [
            new PresenceChannel('conversation.' . $this->conversationId . ':presence'),
        ];
    }

    public function broadcastWith(): array
    {
        return [
            'user_id' => $this->user->id,
            'user_name' => $this->user->name,
        ];
    }
}

Listen on frontend:

window.Echo.join(`conversation.${conversationId}:presence`)
    .here((users) => {
        console.log('Currently online:', users);
    })
    .joining((user) => {
        console.log(user.name + ' joined');
    })
    .leaving((user) => {
        console.log(user.name + ' left');
    })
    .listen('UserTyping', (data) => {
        showTypingIndicator(data.user_name);
    });

Real-Time Notifications

class OrderShipped implements ShouldBroadcast
{
    public function __construct(
        public Order $order,
    ) {}

    public function broadcastOn(): array
    {
        return [
            new PrivateChannel('user.' . $this->order->user_id),
        ];
    }

    public function broadcastAs(): string
    {
        return 'order.shipped';
    }

    public function broadcastWith(): array
    {
        return [
            'order_id' => $this->order->id,
            'tracking_number' => $this->order->tracking_number,
        ];
    }
}

Frontend:

window.Echo.private(`user.${userId}`)
    .notification((notification) => {
        console.log('Notification:', notification);
    })
    .listen('order.shipped', (data) => {
        showNotification(`Order #${data.order_id} has shipped!`);
    });

Debouncing Broadcasts

Prevent too many events:

class UserSearching implements ShouldBroadcast
{
    public function __construct(
        public string $query,
        public int $userId,
    ) {}

    public function broadcastOn(): array
    {
        return [new Channel('search')];
    }
}

// Controller
public function search(Request $request)
{
    event(new UserSearching($request->query, auth()->id()));

    return SearchService::find($request->query);
}

Frontend with debounce:

import { debounce } from 'lodash-es';

const search = debounce((query) => {
    window.Echo.channel('search')
        .listen('UserSearching', (data) => {
            updateSearchResults(data);
        });
}, 300);

document.getElementById('search').addEventListener('input', (e) => {
    search(e.target.value);
});

Conditional Broadcasting

Only broadcast under certain conditions:

class PostPublished implements ShouldBroadcast
{
    public function __construct(public Post $post) {}

    public function broadcastOn(): array
    {
        return $this->post->is_public
            ? [new Channel('posts')]
            : [];
    }
}

Or check on the fly:

public function broadcastWhen(): bool
{
    return $this->post->is_published && !$this->post->is_scheduled;
}

Broadcasting Model Changes

class Post extends Model
{
    protected $dispatchesEvents = [
        'created' => PostCreated::class,
        'updated' => PostUpdated::class,
        'deleted' => PostDeleted::class,
    ];
}

class PostUpdated implements ShouldBroadcast
{
    public function __construct(public Post $post) {}

    public function broadcastOn(): array
    {
        return [new PrivateChannel('posts.' . $this->post->user_id)];
    }

    public function broadcastWith(): array
    {
        return $this->post->only(['id', 'title', 'content', 'updated_at']);
    }
}

Testing Broadcasting

use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\Facades\Event;

public function test_message_is_broadcast()
{
    Event::fake();

    $message = Message::factory()->create();

    event(new MessageSent($message));

    Event::assertDispatched(MessageSent::class);
}

Performance Optimization

Queue Broadcasting

Prevent blocking requests during broadcast:

class MessageSent implements ShouldBroadcast, ShouldQueue
{
    use Dispatchable, InteractsWithBroadcasting;
    
    public $queue = 'broadcasts';
}

Compress Data

Only broadcast what's necessary:

public function broadcastWith(): array
{
    return [
        'id' => $this->message->id,
        'text' => $this->message->text,
        'user_id' => $this->message->user->id,
        // Don't include heavy relationships
    ];
}

Batch Broadcasting

public function broadcastMultiple(array $userIds)
{
    foreach ($userIds as $userId) {
        event(new Notification($userId, $data));
    }
    
    // Or use batch
    Bus::batch(array_map(
        fn($id) => new BroadcastNotification($id, $data),
        $userIds
    ))->dispatch();
}

Best Practices

  1. Validate channel access - Always check authorization
  2. Compress data - Don't broadcast huge payloads
  3. Use queues - Broadcast asynchronously
  4. Limit listeners - Don't broadcast to everyone
  5. Handle reconnections - Client-side retry logic
  6. Monitor connections - Track active users
  7. Test thoroughly - Test broadcasting logic

Common Patterns

Live Comments

class CommentPosted implements ShouldBroadcast
{
    public function broadcastOn()
    {
        return new PrivateChannel('post.' . $this->comment->post_id);
    }
}

Live Notifications

class Notification implements ShouldBroadcast, ShouldQueue
{
    public function broadcastOn()
    {
        return new PrivateChannel('user.' . $this->user_id);
    }
}

Collaboration

class DocumentUpdated implements ShouldBroadcast
{
    public function broadcastOn()
    {
        return new PrivateChannel('document.' . $this->document_id);
    }
}

Summary

Laravel Broadcasting with Reverb enables real-time features:

  • Public channels - Broadcast to everyone
  • Private channels - Restrict to specific users
  • Presence channels - Track online users
  • Queue broadcasting - Non-blocking broadcasts
  • Model events - Auto-broadcast on changes
  • Client-side listening - JavaScript integration

Build responsive, real-time applications with ease.

Comments