Building Real-Time Apps with Laravel Reverb and Echo
HTTP is great for request-response patterns. It's not great for "tell me immediately when something changes." That's where WebSockets come in -- and that's exactly what Laravel Reverb was built for.
Reverb is Laravel's first-party, high-performance WebSocket server. It uses the Pusher protocol under the hood, which means it works seamlessly with Laravel's existing broadcasting system and Laravel Echo. One server can handle thousands of simultaneous connections. You don't need a third-party service.
In this tutorial, we'll build a real-time notification system from scratch. By the end, authenticated users will receive instant browser notifications when something happens in your application -- no polling, no page refreshes.
Prerequisites
- Laravel 11 or newer (Reverb ships with Laravel and is supported from 11+)
- A fresh or existing Laravel application
- Node.js and npm for the frontend
Step 1: Install and Configure Reverb
Starting with Laravel 11, Reverb can be installed with a single Artisan command:
php artisan install:broadcasting
This command will:
- Install the
laravel/reverbpackage via Composer - Publish the Reverb configuration file to
config/reverb.php - Add the necessary environment variables to your
.envfile - Install and configure Laravel Echo with the
laravel-echoandpusher-jsnpm packages
After running the command, your .env will have these new variables:
BROADCAST_CONNECTION=reverb
REVERB_APP_ID=your-app-id
REVERB_APP_KEY=your-app-key
REVERB_APP_SECRET=your-app-secret
REVERB_HOST="localhost"
REVERB_PORT=8080
REVERB_SCHEME=http
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
The VITE_ prefixed variables expose the connection config to your frontend JavaScript via Vite.
Step 2: Create a Broadcastable Event
Laravel's broadcasting system lets you fire events on the server and have them automatically pushed to connected clients. Let's create a notification event.
php artisan make:event OrderStatusUpdated
Open the generated event at app/Events/OrderStatusUpdated.php and implement ShouldBroadcast:
<?php
namespace App\Events;
use App\Models\Order;
use App\Models\User;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderStatusUpdated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public readonly Order $order,
public readonly User $user,
) {}
public function broadcastOn(): array
{
// Private channel scoped to the specific user
return [
new PrivateChannel('users.' . $this->user->id),
];
}
public function broadcastWith(): array
{
return [
'order_id' => $this->order->id,
'status' => $this->order->status,
'message' => "Order #{$this->order->id} is now {$this->order->status}.",
];
}
}
A few things to note here. The event broadcasts on a private channel scoped to the specific user.
Private channels require authentication -- only the intended user can subscribe. The broadcastWith()
method defines exactly what data gets sent to the client. Keep this lean; don't broadcast entire model
objects if you only need a few fields.
Step 3: Authorize the Private Channel
Private channels need an authorization rule so Laravel knows who can subscribe. Open
routes/channels.php:
use App\Models\User;
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('users.{userId}', function (User $user, int $userId) {
return (int) $user->id === $userId;
});
This closure returns true if the authenticated user's ID matches the channel's {userId} parameter.
Only the correct user can subscribe. If you return false, the subscription is rejected.
Step 4: Fire the Event
Dispatching a broadcastable event is identical to dispatching any other Laravel event:
use App\Events\OrderStatusUpdated;
// In a controller, job, or service class
event(new OrderStatusUpdated($order, $order->user));
Or if you want to fire it via the queue (recommended for production -- it offloads the WebSocket push to a queue worker):
// The event will be dispatched asynchronously via your queue
OrderStatusUpdated::dispatch($order, $order->user);
If your event implements ShouldBroadcastNow instead of ShouldBroadcast, it bypasses the queue and
broadcasts immediately. Useful for development and for events where real-time delivery is genuinely
time-critical.
Step 5: Set Up the Frontend with Laravel Echo
Laravel Echo is the JavaScript library that listens for events on your WebSocket channels. After running
install:broadcasting, Echo is already configured. You'll find the setup in 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'],
});
This is auto-generated by install:broadcasting. You shouldn't need to modify it.
Now, in your component or page JavaScript, subscribe to the private channel and listen for the event:
// resources/js/app.js or in a component
const userId = document.querySelector('meta[name="user-id"]')?.content;
if (userId) {
window.Echo.private(`users.${userId}`)
.listen('OrderStatusUpdated', (event) => {
console.log('Order updated!', event);
showNotification(event.message);
});
}
function showNotification(message) {
// Insert your notification UI logic here
const container = document.getElementById('notifications');
const notification = document.createElement('div');
notification.className = 'notification';
notification.textContent = message;
container.prepend(notification);
}
Make sure your Blade layout outputs the user ID in a meta tag:
<meta name="user-id" content="{{ auth()->id() }}">
Step 6: Start the Reverb Server
Run the Reverb WebSocket server in a separate terminal:
php artisan reverb:start
For development, this is all you need. For production, you'll want to run Reverb under a process manager like Supervisor to keep it alive.
A basic Supervisor config for Reverb:
[program:reverb]
command=php /var/www/html/artisan reverb:start
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/reverb.log
Step 7: Test the Whole Flow
Start all the pieces:
# Terminal 1: Laravel dev server
php artisan serve
# Terminal 2: Reverb WebSocket server
php artisan reverb:start
# Terminal 3: Queue worker (to process broadcast jobs)
php artisan queue:work
# Terminal 4: Vite asset compilation
npm run dev
Log in to your app, open the browser console, and fire the event from Tinker:
php artisan tinker
$order = App\Models\Order::first();
event(new App\Events\OrderStatusUpdated($order, $order->user));
Watch the browser console light up. You should see the event data come through in real time.
Presence Channels: Knowing Who's Online
For features like "who's currently viewing this document," Reverb supports presence channels. These are like private channels but they track active members.
// In routes/channels.php
Broadcast::channel('document.{documentId}', function (User $user, int $documentId) {
// Return user data to share with other subscribers
return ['id' => $user->id, 'name' => $user->name];
});
// Frontend
window.Echo.join(`document.${documentId}`)
.here((users) => {
console.log('Currently viewing:', users);
})
.joining((user) => {
console.log(user.name + ' joined');
})
.leaving((user) => {
console.log(user.name + ' left');
});
Going to Production
For production deployments, a few additional steps are needed.
SSL/TLS: Reverb should run behind a reverse proxy (Nginx or Caddy) that handles TLS termination.
Set REVERB_SCHEME=https and REVERB_PORT=443 in your production .env.
Horizontal Scaling: If you need to run multiple Reverb instances, you can use the Redis publish
driver to synchronize state across servers. Configure REVERB_SCALING_ENABLED=true and point it at
your Redis instance.
Laravel Cloud: Reverb is supported natively in Laravel Cloud with managed WebSocket hosting powered by Reverb, announced at the same time as Laravel 12.
Wrapping Up
Laravel Reverb removes every excuse for not building real-time features. It's first-party, it's fast, it runs on your own infrastructure, and the integration with Laravel's existing broadcasting system means most of the patterns you already know -- events, listeners, channels -- carry straight over.
For the complete reference, the official Reverb documentation is thorough and well-maintained.
Sources: