Laravel Framework

Building Real-time Features with Broadcasting

18 min Lesson 32 of 45

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 NewNotification broadcast 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 DashboardUpdated event 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 DocumentUpdated event 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.