Building Real-time Features with Broadcasting
Introduction to Broadcasting
Broadcasting in Laravel allows you to broadcast server-side events to your client-side JavaScript application using WebSockets. This enables real-time features like notifications, chat messages, live updates, and collaborative editing without the need for polling or constant HTTP requests.
Real-time Use Cases:
- Live notifications (new messages, orders, alerts)
- Chat applications and messaging systems
- Real-time dashboards and analytics
- Collaborative editing (documents, whiteboards)
- Live comments and reactions
- Presence tracking (who's online)
- Live feeds and activity streams
Broadcasting Architecture
Laravel's broadcasting system consists of several components:
- Events: PHP classes that represent something that happened in your application
- Broadcast Channels: Named pathways where events are broadcast (public, private, presence)
- Queue Workers: Process broadcast jobs asynchronously
- Broadcasting Driver: Service that handles WebSocket connections (Pusher, Ably, Redis, Soketi)
- Laravel Echo: JavaScript library that listens for broadcast events on the client-side
Setting Up Broadcasting
First, configure your broadcasting driver in the .env file:
# .env - Using Pusher
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=your-app-id
PUSHER_APP_KEY=your-app-key
PUSHER_APP_SECRET=your-app-secret
PUSHER_APP_CLUSTER=mt1
# Or using Redis (requires predis/predis package)
BROADCAST_DRIVER=redis
Install the required packages:
# Install Pusher PHP SDK
composer require pusher/pusher-php-server
# Install Laravel Echo and Pusher JS (frontend)
npm install --save-dev laravel-echo pusher-js
# For Redis broadcasting
composer require predis/predis
Enable broadcasting in config/app.php by uncommenting the BroadcastServiceProvider:
// config/app.php
'providers' => [
// ...
App\Providers\BroadcastServiceProvider::class,
],
Soketi - Free Alternative: Soketi is a free, open-source Pusher alternative that you can self-host. It's perfect for development and production without monthly fees. Install with: npm install -g @soketi/soketi and run with: soketi start
Creating Broadcast Events
Create an event that implements the ShouldBroadcast interface:
php artisan make:event OrderShipped
<?php
// app/Events/OrderShipped.php
namespace App\Events;
use App\Models\Order;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderShipped implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $order;
public function __construct(Order $order)
{
$this->order = $order;
}
// Define which channel(s) to broadcast on
public function broadcastOn()
{
// Public channel - anyone can listen
return new Channel('orders');
// Private channel - requires authentication
// return new PrivateChannel('orders.' . $this->order->user_id);
// Presence channel - tracks who is listening
// return new PresenceChannel('orders');
}
// Customize event name (optional)
public function broadcastAs()
{
return 'order.shipped';
}
// Customize broadcasted data (optional)
public function broadcastWith()
{
return [
'order_id' => $this->order->id,
'tracking_number' => $this->order->tracking_number,
'status' => $this->order->status,
'shipped_at' => $this->order->shipped_at,
];
}
}
Dispatch the event from your controller or anywhere in your application:
<?php
// app/Http/Controllers/OrderController.php
namespace App\Http\Controllers;
use App\Models\Order;
use App\Events\OrderShipped;
class OrderController extends Controller
{
public function ship(Order $order)
{
$order->update(['status' => 'shipped', 'shipped_at' => now()]);
// Broadcast the event
event(new OrderShipped($order));
// Or use broadcast() helper
// broadcast(new OrderShipped($order));
return response()->json(['message' => 'Order shipped successfully']);
}
}
Channel Types
Laravel supports three types of broadcast channels:
1. Public Channels
Anyone can listen to public channels without authentication:
public function broadcastOn()
{
return new Channel('public-updates');
}
2. Private Channels
Private channels require authorization. Define authorization logic in routes/channels.php:
<?php
// routes/channels.php
use Illuminate\Support\Facades\Broadcast;
// User-specific channel
Broadcast::channel('orders.{userId}', function ($user, $userId) {
return (int) $user->id === (int) $userId;
});
// Order-specific channel
Broadcast::channel('order.{orderId}', function ($user, $orderId) {
return $user->orders()->where('id', $orderId)->exists();
});
// Admin-only channel
Broadcast::channel('admin', function ($user) {
return $user->is_admin;
});
// Use private channel in event
public function broadcastOn()
{
return new PrivateChannel('orders.' . $this->order->user_id);
}
3. Presence Channels
Presence channels track who is currently subscribed to the channel:
<?php
// routes/channels.php
Broadcast::channel('chat.{roomId}', function ($user, $roomId) {
if ($user->canJoinRoom($roomId)) {
return [
'id' => $user->id,
'name' => $user->name,
'avatar' => $user->avatar_url,
];
}
});
// Use presence channel in event
public function broadcastOn()
{
return new PresenceChannel('chat.1');
}
Important: Make sure your queue worker is running for broadcast events to be processed: php artisan queue:work. Without a queue worker, broadcast events will be processed synchronously and may slow down your application.
Setting Up Laravel Echo (Frontend)
Configure Laravel Echo in your JavaScript to listen for events:
// resources/js/bootstrap.js
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'pusher',
key: import.meta.env.VITE_PUSHER_APP_KEY,
cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
forceTLS: true,
// For authenticated channels
authEndpoint: '/broadcasting/auth',
auth: {
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
}
}
});
// For Soketi (local development)
/*
window.Echo = new Echo({
broadcaster: 'pusher',
key: 'app-key',
wsHost: 'localhost',
wsPort: 6001,
forceTLS: false,
disableStats: true,
enabledTransports: ['ws', 'wss'],
});
*/
// Add to .env
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
Listening for Events
Listen for broadcast events in your JavaScript:
// Listen to public channel
Echo.channel('orders')
.listen('OrderShipped', (e) => {
console.log('Order shipped:', e.order_id);
showNotification('Your order #' + e.order_id + ' has been shipped!');
});
// Listen to private channel (requires authentication)
Echo.private('orders.' + userId)
.listen('.order.shipped', (e) => {
console.log('Your order shipped:', e);
updateOrderStatus(e.order_id, 'shipped');
});
// Listen to presence channel
Echo.join('chat.1')
.here((users) => {
// Users currently in the channel
console.log('Users online:', users);
displayOnlineUsers(users);
})
.joining((user) => {
// User joined the channel
console.log(user.name + ' joined');
addOnlineUser(user);
})
.leaving((user) => {
// User left the channel
console.log(user.name + ' left');
removeOnlineUser(user);
})
.listen('MessageSent', (e) => {
// Chat message received
displayMessage(e.message);
});
Event Naming: When using broadcastAs(), prefix the event name with a dot when listening: .listen('.order.shipped'). Without broadcastAs(), use the class name: .listen('OrderShipped')
Real-time Notifications
Laravel makes it easy to send broadcast notifications:
<?php
// app/Notifications/InvoicePaid.php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\BroadcastMessage;
use Illuminate\Notifications\Notification;
class InvoicePaid extends Notification implements ShouldQueue
{
use Queueable;
public $invoice;
public function __construct($invoice)
{
$this->invoice = $invoice;
}
public function via($notifiable)
{
return ['database', 'broadcast'];
}
public function toArray($notifiable)
{
return [
'invoice_id' => $this->invoice->id,
'amount' => $this->invoice->amount,
'message' => 'Invoice #' . $this->invoice->id . ' has been paid',
];
}
public function toBroadcast($notifiable)
{
return new BroadcastMessage([
'invoice_id' => $this->invoice->id,
'amount' => $this->invoice->amount,
'message' => 'Invoice #' . $this->invoice->id . ' has been paid',
]);
}
}
// Send notification
$user->notify(new InvoicePaid($invoice));
// Listen for notifications on frontend
Echo.private('App.Models.User.' + userId)
.notification((notification) => {
console.log('Notification received:', notification);
showNotification(notification.message);
updateNotificationBadge();
});
Building a Real-time Chat
Here's a complete example of a real-time chat system:
<?php
// app/Events/MessageSent.php
namespace App\Events;
use App\Models\Message;
use App\Models\User;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class MessageSent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $message;
public $user;
public function __construct(Message $message, User $user)
{
$this->message = $message;
$this->user = $user;
}
public function broadcastOn()
{
return new PresenceChannel('chat.' . $this->message->chat_room_id);
}
public function broadcastWith()
{
return [
'id' => $this->message->id,
'body' => $this->message->body,
'created_at' => $this->message->created_at->toDateTimeString(),
'user' => [
'id' => $this->user->id,
'name' => $this->user->name,
'avatar' => $this->user->avatar_url,
],
];
}
}
<?php
// app/Http/Controllers/ChatController.php
namespace App\Http\Controllers;
use App\Models\Message;
use App\Events\MessageSent;
use Illuminate\Http\Request;
class ChatController extends Controller
{
public function sendMessage(Request $request)
{
$validated = $request->validate([
'chat_room_id' => 'required|exists:chat_rooms,id',
'body' => 'required|string|max:1000',
]);
$message = Message::create([
'user_id' => auth()->id(),
'chat_room_id' => $validated['chat_room_id'],
'body' => $validated['body'],
]);
broadcast(new MessageSent($message, auth()->user()))->toOthers();
return response()->json($message);
}
}
// Frontend JavaScript
const chatRoomId = 1;
// Join chat room
Echo.join(`chat.${chatRoomId}`)
.here((users) => {
displayOnlineUsers(users);
})
.joining((user) => {
addOnlineUser(user);
showSystemMessage(`${user.name} joined the chat`);
})
.leaving((user) => {
removeOnlineUser(user);
showSystemMessage(`${user.name} left the chat`);
})
.listen('MessageSent', (e) => {
appendMessage(e);
});
// Send message
function sendMessage(body) {
axios.post('/chat/message', {
chat_room_id: chatRoomId,
body: body
}).then(response => {
appendMessage(response.data);
clearInput();
});
}
function appendMessage(message) {
const messageHtml = `
<div class="message">
<img src="${message.user.avatar}" alt="${message.user.name}">
<div>
<strong>${message.user.name}</strong>
<p>${message.body}</p>
<span>${message.created_at}</span>
</div>
</div>
`;
document.querySelector('#messages').insertAdjacentHTML('beforeend', messageHtml);
}
Performance Tip: Use the toOthers() method when broadcasting to prevent the sender from receiving their own message twice: broadcast(new MessageSent($message))->toOthers()
Practice Exercise 1: Real-time Notification System
Build a real-time notification system with the following features:
- Create a
NewNotificationbroadcast event - Send notifications when users receive new messages, follows, or likes
- Display notifications in a dropdown bell icon (with counter)
- Mark notifications as read when clicked
- Use private channels for user-specific notifications
- Store notifications in database using Laravel's notification system
Hint: Use Echo.private('App.Models.User.' + userId).notification() to listen for notifications.
Practice Exercise 2: Live Dashboard Updates
Create a live dashboard that updates in real-time:
- Display metrics: total orders, revenue, active users
- Broadcast
DashboardUpdatedevent when metrics change - Update dashboard without page refresh when event is received
- Add chart that updates in real-time (use Chart.js)
- Show "Live" indicator when connected to WebSocket
- Handle reconnection when connection is lost
Bonus: Add presence channel to show other admins viewing the dashboard.
Practice Exercise 3: Collaborative Document Editing
Build a simple collaborative editor:
- Create
DocumentUpdatedevent that broadcasts changes - Show cursor positions of other users (presence channel)
- Broadcast text changes with debouncing (wait 500ms after typing stops)
- Display who is currently editing the document
- Handle conflicts when multiple users edit simultaneously
- Save document to database periodically
Challenge: Implement operational transformation or CRDT for conflict resolution.
Summary
In this lesson, you learned how to build real-time features in Laravel using broadcasting. Key concepts include:
- Understanding broadcasting architecture and WebSocket communication
- Setting up broadcasting with Pusher, Soketi, or Redis
- Creating broadcast events that implement ShouldBroadcast
- Working with public, private, and presence channels
- Authorizing private channels in routes/channels.php
- Configuring Laravel Echo on the frontend
- Listening for events and handling real-time updates
- Building real-time notifications, chat, and dashboards
- Tracking online presence with presence channels
Broadcasting enables you to create engaging, modern applications with live updates that enhance user experience. In the next lesson, we'll explore advanced Blade component techniques to build reusable UI elements.