Laravel Framework
Laravel Reverb & WebSockets
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:
- Install and configure Laravel Reverb
- Create a NotificationSent event that broadcasts to user-specific private channels
- Set up channel authorization for private user channels
- Create a notification bell component that shows real-time counts
- Display toast notifications when new notifications arrive
- Add a "mark as read" feature that updates in real-time
Exercise 2: Create a collaborative document editor:
- Set up a presence channel showing all active editors
- Broadcast content changes in real-time to all editors
- Use whisper events for cursor position tracking
- Implement typing indicators showing who's editing which section
- Add conflict resolution for simultaneous edits
- Show user avatars with cursor positions
Exercise 3: Build a live dashboard with real-time metrics:
- Create events for various metrics (sales, users online, server stats)
- Broadcast metrics every 5 seconds using a scheduled job
- Build a dashboard with charts that update in real-time
- Add sound notifications for important events
- Implement reconnection logic if WebSocket disconnects
- Display connection status indicator