Real-time with WebSockets & Reverb
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',
],
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}")
),
];
}
}
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']);
}
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:
- Create a notification for new followers (FollowerNotification)
- Broadcast to the followed user's private channel
- Store notifications in database and broadcast simultaneously
- Implement frontend listener that updates badge count and shows toast
- Add API endpoints to fetch notifications and mark as read
- Test with multiple users and verify real-time delivery
Exercise 2: Live Dashboard Updates
Create a real-time admin dashboard:
- Broadcast OrderPlaced events to a public "order-stats" channel
- Include data: order count, total revenue, recent orders
- Use Echo to listen and update dashboard charts in real-time
- Add multiple broadcast events: UserRegistered, PaymentReceived
- Implement auto-refresh for critical metrics every 30 seconds
- Test dashboard updates when orders are placed
Exercise 3: Collaborative Text Editor
Build a basic collaborative editing feature:
- Create a presence channel for document editing ("document.{id}")
- Show who's currently viewing/editing the document
- Use whisper events to sync cursor positions between users
- Broadcast text changes (debounced) to all participants
- Add "User X is editing..." indicators
- 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.