Advanced Laravel Broadcasting: Real-Time Applications
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
- Validate channel access - Always check authorization
- Compress data - Don't broadcast huge payloads
- Use queues - Broadcast asynchronously
- Limit listeners - Don't broadcast to everyone
- Handle reconnections - Client-side retry logic
- Monitor connections - Track active users
- 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.