Advanced Laravel

Real-time with WebSockets & Reverb

18 min Lesson 17 of 40

Real-time with WebSockets & Reverb

Real-time features transform user experiences by delivering instant updates without page refreshes. Laravel Reverb, Laravel's official WebSocket server, makes building real-time applications incredibly simple. In this lesson, we'll explore broadcasting events, managing channels, implementing Laravel Echo on the frontend, and building real-world features like notifications and chat systems.

Laravel Reverb Setup

Laravel Reverb is a first-party WebSocket server that seamlessly integrates with Laravel's broadcasting system. It provides a scalable, performant alternative to third-party services like Pusher.

# Install Laravel Reverb
composer require laravel/reverb

# Publish configuration
php artisan reverb:install

# This creates:
# - config/reverb.php (Reverb configuration)
# - Updates .env with REVERB_* variables
# - Updates config/broadcasting.php

# .env configuration
BROADCAST_CONNECTION=reverb

REVERB_APP_ID=my-app-id
REVERB_APP_KEY=my-app-key
REVERB_APP_SECRET=my-app-secret
REVERB_HOST=localhost
REVERB_PORT=8080
REVERB_SCHEME=http

# For production
REVERB_HOST=reverb.example.com
REVERB_PORT=443
REVERB_SCHEME=https

# Start Reverb server
php artisan reverb:start

# Start with debug mode
php artisan reverb:start --debug

# Run in background (production)
nohup php artisan reverb:start > /dev/null 2>&1 &

<?php
// config/broadcasting.php
'connections' => [
    'reverb' => [
        'driver' => 'reverb',
        'key' => env('REVERB_APP_KEY'),
        'secret' => env('REVERB_APP_SECRET'),
        'app_id' => env('REVERB_APP_ID'),
        'options' => [
            'host' => env('REVERB_HOST', 'localhost'),
            'port' => env('REVERB_PORT', 8080),
            'scheme' => env('REVERB_SCHEME', 'http'),
            'useTLS' => env('REVERB_SCHEME', 'http') === 'https',
        ],
    ],
],

// Enable queue for broadcasts (recommended)
'queue' => [
    'connection' => env('QUEUE_CONNECTION', 'redis'),
    'queue' => 'broadcasts',
],
Production Tip: Use a process manager like Supervisor to keep Reverb running in production. Configure SSL/TLS certificates for secure WebSocket connections (wss://) to prevent mixed-content warnings in browsers.

Broadcasting Events

Laravel's event broadcasting allows server-side events to be pushed to client-side JavaScript applications in real-time.

<?php

// Create a broadcast event
php artisan make:event MessageSent

// app/Events/MessageSent.php
namespace App\Events;

use App\Models\Message;
use App\Models\User;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels;

class MessageSent implements ShouldBroadcast
{
    use InteractsWithSockets, SerializesModels;

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

    // Define which channels to broadcast on
    public function broadcastOn(): array
    {
        return [
            new PrivateChannel('chat.' . $this->message->chat_id),
        ];
    }

    // Customize broadcast event name (default: MessageSent)
    public function broadcastAs(): string
    {
        return 'message.sent';
    }

    // Customize broadcast data
    public function broadcastWith(): array
    {
        return [
            'id' => $this->message->id,
            'content' => $this->message->content,
            'sender' => [
                'id' => $this->sender->id,
                'name' => $this->sender->name,
                'avatar' => $this->sender->avatar_url,
            ],
            'timestamp' => $this->message->created_at->toIso8601String(),
        ];
    }

    // Queue broadcast for better performance
    public function broadcastQueue(): string
    {
        return 'broadcasts';
    }
}

// Trigger the event
use App\Events\MessageSent;

public function sendMessage(Request $request)
{
    $message = Message::create([
        'chat_id' => $request->chat_id,
        'user_id' => auth()->id(),
        'content' => $request->content,
    ]);

    // Broadcast to all subscribers
    broadcast(new MessageSent($message, auth()->user()));

    return response()->json($message, 201);
}

// Conditional broadcasting
class OrderShipped implements ShouldBroadcast
{
    // Only broadcast if order value is high
    public function broadcastWhen(): bool
    {
        return $this->order->value > 1000;
    }
}

// Broadcast to specific users
public function broadcastOn(): array
{
    return [
        new PrivateChannel('user.' . $this->order->user_id),
        new PrivateChannel('admin-notifications'),
    ];
}

Channel Types: Public, Private & Presence

Laravel supports three channel types, each with different authorization requirements and use cases.

<?php

// routes/channels.php - Define channel authorization

// Public Channel - No authorization required
// Used for: public announcements, status updates
Broadcast::channel('announcements', function () {
    return true; // Everyone can listen
});

// Private Channel - Authorization required
// Used for: user-specific data, private conversations
Broadcast::channel('chat.{chatId}', function ($user, $chatId) {
    // Verify user is a member of this chat
    return $user->chats()->where('id', $chatId)->exists();
});

Broadcast::channel('user.{userId}', function ($user, $userId) {
    // Users can only listen to their own channel
    return (int) $user->id === (int) $userId;
});

// Presence Channel - Like private, but tracks who's online
// Used for: chat rooms, collaborative editing, online indicators
Broadcast::channel('chat-room.{roomId}', function ($user, $roomId) {
    if ($user->canAccessRoom($roomId)) {
        return [
            'id' => $user->id,
            'name' => $user->name,
            'avatar' => $user->avatar_url,
            'status' => 'online',
        ];
    }
});

// Complex authorization with model binding
Broadcast::channel('project.{project}', function ($user, Project $project) {
    return $user->id === $project->user_id || $project->team->users->contains($user);
});

// Event using different channel types
class UserStatusChanged implements ShouldBroadcast
{
    public function broadcastOn(): array
    {
        return [
            // Public: global user count
            new Channel('user-stats'),
            // Private: notify user's friends
            ...collect($this->user->friends)->map(fn($friend) =>
                new PrivateChannel("user.{$friend->id}")
            ),
            // Presence: update all rooms user is in
            ...collect($this->user->activeRooms)->map(fn($room) =>
                new PresenceChannel("chat-room.{$room->id}")
            ),
        ];
    }
}
Security Tip: Always implement proper authorization in routes/channels.php. Never return true for private channels without verifying user permissions. Use model route binding for cleaner, more secure authorization logic.

Laravel Echo - Frontend Integration

Laravel Echo is a JavaScript library that makes subscribing to channels and listening for events broadcast by Laravel seamless on the frontend.

# Install Laravel Echo and Pusher JS (used for protocol)
npm install --save-dev laravel-echo pusher-js

// resources/js/bootstrap.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 ?? 80,
    wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
    forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
    enabledTransports: ['ws', 'wss'],
});

// .env for Vite
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"

// Listen to public channels
Echo.channel('announcements')
    .listen('AnnouncementMade', (e) => {
        console.log('New announcement:', e.title, e.message);
        showNotification(e.title, e.message);
    });

// Listen to private channels (requires authentication)
Echo.private(`user.${userId}`)
    .listen('.message.sent', (e) => {
        console.log('New message:', e);
        appendMessage(e);
    })
    .listen('NotificationCreated', (e) => {
        incrementNotificationBadge();
        showToast(e.notification);
    });

// Join presence channels (see who's online)
Echo.join(`chat-room.${roomId}`)
    .here((users) => {
        // Called when joining - list of users already in channel
        console.log('Users currently online:', users);
        updateOnlineUsers(users);
    })
    .joining((user) => {
        // Called when someone joins
        console.log(user.name + ' joined');
        addUserToOnlineList(user);
    })
    .leaving((user) => {
        // Called when someone leaves
        console.log(user.name + ' left');
        removeUserFromOnlineList(user);
    })
    .listen('.message.sent', (e) => {
        appendMessage(e);
    })
    .error((error) => {
        console.error('Channel error:', error);
    });

// Leave a channel when component unmounts
const channel = Echo.private(`chat.${chatId}`);

// Later...
Echo.leave(`chat.${chatId}`);

// Whisper events (client-to-client without server)
// Useful for typing indicators
Echo.private(`chat.${chatId}`)
    .whisper('typing', {
        user: currentUser.name
    });

Echo.private(`chat.${chatId}`)
    .listenForWhisper('typing', (e) => {
        showTypingIndicator(e.user);
    });

// React/Vue example
useEffect(() => {
    Echo.private(`user.${user.id}`)
        .listen('.message.sent', handleNewMessage)
        .notification((notification) => {
            toast.success(notification.message);
        });

    return () => {
        Echo.leave(`user.${user.id}`);
    };
}, [user.id]);

Real-time Notifications

Laravel's notification system integrates seamlessly with broadcasting to deliver instant in-app notifications.

<?php

// Create a notification
php artisan make:notification NewCommentNotification

// app/Notifications/NewCommentNotification.php
namespace App\Notifications;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Messages\BroadcastMessage;
use Illuminate\Notifications\Notification;

class NewCommentNotification extends Notification implements ShouldBroadcast
{
    public function __construct(
        public $comment,
        public $post
    ) {}

    public function via($notifiable): array
    {
        return ['database', 'broadcast'];
    }

    public function toDatabase($notifiable): array
    {
        return [
            'comment_id' => $this->comment->id,
            'post_id' => $this->post->id,
            'commenter' => $this->comment->user->name,
            'message' => $this->comment->user->name . ' commented on your post',
        ];
    }

    public function toBroadcast($notifiable): BroadcastMessage
    {
        return new BroadcastMessage([
            'id' => $this->id,
            'comment_id' => $this->comment->id,
            'post_id' => $this->post->id,
            'commenter' => [
                'name' => $this->comment->user->name,
                'avatar' => $this->comment->user->avatar_url,
            ],
            'message' => $this->comment->user->name . ' commented on your post',
            'created_at' => now()->toIso8601String(),
        ]);
    }

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

// Send notification
$post->user->notify(new NewCommentNotification($comment, $post));

// Frontend - Listen for notifications
Echo.private(`user.${userId}`)
    .notification((notification) => {
        console.log('New notification:', notification);

        // Update UI
        addNotificationToList(notification);
        incrementBadge();
        playSound();

        // Show toast
        toast.info(notification.message, {
            action: {
                label: 'View',
                onClick: () => window.location.href = `/posts/${notification.post_id}`
            }
        });
    });

// Fetch unread notifications
async function fetchNotifications() {
    const response = await fetch('/api/notifications');
    const notifications = await response.json();
    renderNotifications(notifications);
}

// Mark as read
async function markAsRead(notificationId) {
    await fetch(`/api/notifications/${notificationId}/read`, {
        method: 'POST'
    });
}

// API endpoint for notifications
public function index()
{
    return auth()->user()->notifications()->latest()->paginate(20);
}

public function markAsRead($id)
{
    auth()->user()->notifications()->findOrFail($id)->markAsRead();
    return response()->json(['message' => 'Marked as read']);
}
Performance Warning: Always queue broadcast notifications using ShouldQueue interface. Broadcasting synchronously can slow down your application significantly, especially with many recipients.

Building a Real-time Chat System

A complete chat implementation combines private channels, presence tracking, and whisper events for a seamless real-time experience.

<?php

// app/Http/Controllers/ChatController.php
class ChatController extends Controller
{
    public function sendMessage(Request $request, $chatId)
    {
        $validated = $request->validate([
            'content' => 'required|string|max:1000',
        ]);

        $message = Message::create([
            'chat_id' => $chatId,
            'user_id' => auth()->id(),
            'content' => $validated['content'],
        ]);

        $message->load('user');

        broadcast(new MessageSent($message))->toOthers();

        return response()->json($message, 201);
    }

    public function getMessages($chatId)
    {
        $messages = Message::where('chat_id', $chatId)
            ->with('user')
            ->latest()
            ->paginate(50);

        return response()->json($messages);
    }
}

// Frontend - Complete chat component
class ChatRoom {
    constructor(chatId, userId) {
        this.chatId = chatId;
        this.userId = userId;
        this.typingTimeout = null;
        this.initializeEcho();
        this.loadMessages();
    }

    initializeEcho() {
        // Join presence channel
        this.channel = Echo.join(`chat.${this.chatId}`)
            .here((users) => {
                this.renderOnlineUsers(users);
            })
            .joining((user) => {
                this.addOnlineUser(user);
                this.showSystemMessage(`${user.name} joined`);
            })
            .leaving((user) => {
                this.removeOnlineUser(user);
                this.showSystemMessage(`${user.name} left`);
            })
            .listen('.message.sent', (e) => {
                this.appendMessage(e);
                this.scrollToBottom();
                this.playNotificationSound();
            })
            .listenForWhisper('typing', (e) => {
                this.showTypingIndicator(e.user);
                this.hideTypingIndicatorAfterDelay();
            });
    }

    async loadMessages() {
        const response = await fetch(`/api/chats/${this.chatId}/messages`);
        const data = await response.json();
        this.renderMessages(data.data);
    }

    async sendMessage(content) {
        const response = await fetch(`/api/chats/${this.chatId}/messages`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
            },
            body: JSON.stringify({ content })
        });

        const message = await response.json();
        this.appendMessage(message);
        this.scrollToBottom();
    }

    onTyping(userName) {
        clearTimeout(this.typingTimeout);

        this.channel.whisper('typing', {
            user: userName
        });

        this.typingTimeout = setTimeout(() => {
            this.channel.whisper('stopped-typing', {
                user: userName
            });
        }, 2000);
    }

    destroy() {
        Echo.leave(`chat.${this.chatId}`);
    }
}

// Usage
const chat = new ChatRoom(chatId, userId);

// Send message
document.querySelector('#send-btn').addEventListener('click', () => {
    const input = document.querySelector('#message-input');
    chat.sendMessage(input.value);
    input.value = '';
});

// Typing indicator
document.querySelector('#message-input').addEventListener('input', () => {
    chat.onTyping(currentUser.name);
});

// Cleanup on component unmount
window.addEventListener('beforeunload', () => {
    chat.destroy();
});

Exercise 1: Real-time Notification System

Build a complete real-time notification system:

  1. Create a notification for new followers (FollowerNotification)
  2. Broadcast to the followed user's private channel
  3. Store notifications in database and broadcast simultaneously
  4. Implement frontend listener that updates badge count and shows toast
  5. Add API endpoints to fetch notifications and mark as read
  6. Test with multiple users and verify real-time delivery

Exercise 2: Live Dashboard Updates

Create a real-time admin dashboard:

  1. Broadcast OrderPlaced events to a public "order-stats" channel
  2. Include data: order count, total revenue, recent orders
  3. Use Echo to listen and update dashboard charts in real-time
  4. Add multiple broadcast events: UserRegistered, PaymentReceived
  5. Implement auto-refresh for critical metrics every 30 seconds
  6. Test dashboard updates when orders are placed

Exercise 3: Collaborative Text Editor

Build a basic collaborative editing feature:

  1. Create a presence channel for document editing ("document.{id}")
  2. Show who's currently viewing/editing the document
  3. Use whisper events to sync cursor positions between users
  4. Broadcast text changes (debounced) to all participants
  5. Add "User X is editing..." indicators
  6. Test with multiple browser windows to verify collaboration

Summary

In this lesson, you've mastered real-time communication in Laravel using Reverb and WebSockets. You learned how to set up Laravel Reverb, broadcast events to public/private/presence channels, integrate Laravel Echo on the frontend, implement real-time notifications, and build complete features like chat systems with typing indicators and online presence. These skills enable you to create engaging, interactive applications that deliver instant updates to users.