Laravel Framework

Laravel Reverb & WebSockets

18 min Lesson 42 of 45

Laravel Reverb & WebSockets

Laravel Reverb is a first-party WebSocket server for Laravel applications, providing blazing-fast real-time communication. It's built on top of ReactPHP and offers seamless integration with Laravel Broadcasting.

Introduction to Laravel Reverb

Why Use Reverb?
  • Performance: Built in PHP using ReactPHP, optimized for high throughput
  • First-party: Official Laravel package with native integration
  • Cost-effective: Self-hosted alternative to Pusher/Ably
  • Simple setup: Works out-of-the-box with Laravel Broadcasting
  • Horizontal scaling: Can be scaled across multiple servers

Installing and Configuring Reverb

// Install Reverb composer require laravel/reverb // Install Reverb configuration and dependencies php artisan reverb:install // This will: // - Add reverb configuration to config/reverb.php // - Update .env with REVERB_* variables // - Install Laravel Echo and Pusher JS SDK (for client-side) // .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 with SSL REVERB_SCHEME=https REVERB_HOST="your-domain.com"

Starting the Reverb Server

// Development - start Reverb server php artisan reverb:start // With debug output php artisan reverb:start --debug // Production - run as a background process php artisan reverb:start & // Or use Supervisor (recommended for production) // /etc/supervisor/conf.d/reverb.conf [program:reverb] command=php /path/to/artisan reverb:start autostart=true autorestart=true user=www-data redirect_stderr=true stdout_logfile=/var/log/reverb.log
Production Tips:
  • Use Supervisor or systemd to keep Reverb running
  • Put Reverb behind Nginx with WebSocket proxying
  • Enable SSL/TLS for secure WebSocket connections (wss://)
  • Use Redis for horizontal scaling across servers
  • Monitor memory usage and restart periodically

Broadcasting Events with Reverb

// Create a broadcastable event php artisan make:event MessageSent // app/Events/MessageSent.php namespace App\Events; use App\Models\Message; 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 MessageSent implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; public function __construct(public Message $message) { } public function broadcastOn(): array { // Public channel return [ new Channel('chat.' . $this->message->room_id), ]; // Or private channel return [ new PrivateChannel('chat.' . $this->message->room_id), ]; // Or presence channel return [ new PresenceChannel('chat.' . $this->message->room_id), ]; } public function broadcastAs(): string { return 'message.sent'; } public function broadcastWith(): array { return [ 'id' => $this->message->id, 'user' => $this->message->user->only(['id', 'name', 'avatar']), 'content' => $this->message->content, 'created_at' => $this->message->created_at->toISOString(), ]; } } // Dispatch the event use App\Events\MessageSent; $message = Message::create([ 'user_id' => auth()->id(), 'room_id' => $roomId, 'content' => $request->content, ]); broadcast(new MessageSent($message)); // or event(new MessageSent($message));

Client-side Integration with Laravel Echo

// Install Laravel Echo and Pusher JS 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 ?? 8080, wssPort: import.meta.env.VITE_REVERB_PORT ?? 443, forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https', enabledTransports: ['ws', 'wss'], }); // .env 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('chat.1') .listen('.message.sent', (e) => { console.log('New message:', e); appendMessage(e); }); // Listen to private channels (requires authentication) Echo.private('chat.1') .listen('.message.sent', (e) => { appendMessage(e); }); // Leave a channel Echo.leave('chat.1');

Building a Real-time Chat Application

// Controller class ChatController extends Controller { public function sendMessage(Request $request, $roomId) { $request->validate([ 'content' => 'required|string|max:1000', ]); $message = Message::create([ 'user_id' => auth()->id(), 'room_id' => $roomId, 'content' => $request->content, ]); broadcast(new MessageSent($message))->toOthers(); return response()->json($message); } public function typing(Request $request, $roomId) { broadcast(new UserTyping( auth()->user(), $roomId ))->toOthers(); return response()->json(['status' => 'ok']); } } // UserTyping event class UserTyping implements ShouldBroadcast { use Dispatchable, SerializesModels; public function __construct( public User $user, public int $roomId ) {} public function broadcastOn(): array { return [new PrivateChannel('chat.' . $this->roomId)]; } public function broadcastAs(): string { return 'user.typing'; } public function broadcastWith(): array { return [ 'user_id' => $this->user->id, 'name' => $this->user->name, ]; } } // Client-side chat component <div id="chat-room" data-room-id="{{ $room->id }}"> <div id="messages"></div> <div id="typing-indicator"></div> <form id="message-form"> <input type="text" id="message-input" placeholder="Type a message..."> <button type="submit">Send</button> </form> </div> <script> const roomId = document.getElementById('chat-room').dataset.roomId; const messagesDiv = document.getElementById('messages'); const typingDiv = document.getElementById('typing-indicator'); const messageForm = document.getElementById('message-form'); const messageInput = document.getElementById('message-input'); let typingTimeout; // Listen for new messages Echo.private(`chat.${roomId}`) .listen('.message.sent', (e) => { appendMessage(e); }) .listenForWhisper('typing', (e) => { showTyping(e.name); }); // Send message messageForm.addEventListener('submit', async (e) => { e.preventDefault(); const content = messageInput.value.trim(); if (!content) return; await fetch(`/chat/rooms/${roomId}/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, }, body: JSON.stringify({ content }), }); messageInput.value = ''; }); // Typing indicator messageInput.addEventListener('input', () => { Echo.private(`chat.${roomId}`) .whisper('typing', { name: '{{ auth()->user()->name }}' }); clearTimeout(typingTimeout); typingTimeout = setTimeout(() => { typingDiv.textContent = ''; }, 1000); }); function appendMessage(message) { const div = document.createElement('div'); div.className = 'message'; div.innerHTML = ` <strong>${message.user.name}:</strong> <span>${message.content}</span> <small>${new Date(message.created_at).toLocaleTimeString()}</small> `; messagesDiv.appendChild(div); messagesDiv.scrollTop = messagesDiv.scrollHeight; } function showTyping(name) { typingDiv.textContent = `${name} is typing...`; setTimeout(() => { typingDiv.textContent = ''; }, 1000); } </script>

Presence Channels - Who's Online

// Define presence channel authorization in routes/channels.php use Illuminate\Support\Facades\Broadcast; Broadcast::channel('chat.{roomId}', function ($user, $roomId) { if ($user->canAccessRoom($roomId)) { return [ 'id' => $user->id, 'name' => $user->name, 'avatar' => $user->avatar_url, ]; } }); // Client-side - listen to presence channel Echo.join(`chat.${roomId}`) .here((users) => { // Called when joining, receives all current users console.log('Currently in room:', users); updateUserList(users); }) .joining((user) => { // Called when a new user joins console.log(user.name + ' joined'); addUserToList(user); showNotification(`${user.name} joined the room`); }) .leaving((user) => { // Called when a user leaves console.log(user.name + ' left'); removeUserFromList(user); showNotification(`${user.name} left the room`); }) .listen('.message.sent', (e) => { appendMessage(e); }); // User list component function updateUserList(users) { const userList = document.getElementById('user-list'); userList.innerHTML = users.map(user => ` <div class="user" data-user-id="${user.id}"> <img src="${user.avatar}" alt="${user.name}"> <span>${user.name}</span> <span class="status online"></span> </div> `).join(''); } function addUserToList(user) { const userList = document.getElementById('user-list'); const userDiv = document.createElement('div'); userDiv.className = 'user'; userDiv.dataset.userId = user.id; userDiv.innerHTML = ` <img src="${user.avatar}" alt="${user.name}"> <span>${user.name}</span> <span class="status online"></span> `; userList.appendChild(userDiv); } function removeUserFromList(user) { const userDiv = document.querySelector(`[data-user-id="${user.id}"]`); if (userDiv) { userDiv.remove(); } }
Performance Considerations:
  • Use toOthers() to prevent echoing events back to sender
  • Implement throttling for high-frequency events (typing indicators)
  • Use whisper for client-to-client communication without server round-trip
  • Optimize payload size - only send necessary data
  • Consider using Redis for better scaling with multiple Reverb servers

Client-to-Client Events (Whisper)

// Whisper events don't hit the server, only other clients Echo.private('chat.1') .whisper('typing', { name: 'John Doe' }); // Listen for whispers Echo.private('chat.1') .listenForWhisper('typing', (e) => { console.log(e.name + ' is typing...'); }); // Real-world example: cursor tracking in collaborative editor const editor = document.getElementById('editor'); let throttleTimeout; editor.addEventListener('mousemove', (e) => { if (throttleTimeout) return; throttleTimeout = setTimeout(() => { Echo.private('document.1') .whisper('cursor-move', { user: '{{ auth()->user()->name }}', x: e.clientX, y: e.clientY, }); throttleTimeout = null; }, 50); // Throttle to max 20 updates per second }); Echo.private('document.1') .listenForWhisper('cursor-move', (e) => { updateRemoteCursor(e.user, e.x, e.y); });
Exercise 1: Build a real-time notification system:
  1. Install and configure Laravel Reverb
  2. Create a NotificationSent event that broadcasts to user-specific private channels
  3. Set up channel authorization for private user channels
  4. Create a notification bell component that shows real-time counts
  5. Display toast notifications when new notifications arrive
  6. Add a "mark as read" feature that updates in real-time
Exercise 2: Create a collaborative document editor:
  1. Set up a presence channel showing all active editors
  2. Broadcast content changes in real-time to all editors
  3. Use whisper events for cursor position tracking
  4. Implement typing indicators showing who's editing which section
  5. Add conflict resolution for simultaneous edits
  6. Show user avatars with cursor positions
Exercise 3: Build a live dashboard with real-time metrics:
  1. Create events for various metrics (sales, users online, server stats)
  2. Broadcast metrics every 5 seconds using a scheduled job
  3. Build a dashboard with charts that update in real-time
  4. Add sound notifications for important events
  5. Implement reconnection logic if WebSocket disconnects
  6. Display connection status indicator