Laravel Magazine

Building Real-Time Apps with Laravel Reverb and Echo

Eric Van Johnson ·
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/reverb package via Composer
  • Publish the Reverb configuration file to config/reverb.php
  • Add the necessary environment variables to your .env file
  • Install and configure Laravel Echo with the laravel-echo and pusher-js npm 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:

Stay Updated

Subscribe to our newsletter

Get latest news, tutorials, community articles and podcast episodes delivered to your inbox.

Weekly articles
We send a new issue of the newsletter every week on Friday.
No spam
We'll never share your email address and you can opt out at any time.