Laravel Daily's

Real-Time Laravel + Vue + Inertia: Adding Live Updates with Reverb and Echo

hero image

An Inertia SPA can feel immediate without becoming a separate frontend and backend project. Laravel still owns routes, authorization, validation, and server-driven props. Vue renders the interface. Inertia connects the two.

Real-time features add another layer. A user should see a notification, order update, or dashboard metric when it happens. They should not need to refresh the page or wait for the next request.

Laravel Reverb provides the WebSocket server. Laravel Echo gives your Vue components a clean client-side API. Together, they let Laravel events travel directly to an open browser.

This approach works well for a PHP web framework application that needs live behavior without building a separate real-time service.

Real-time updates: Why they matter in an Inertia SPA

Traditional request-based interfaces update after a form submission, navigation, or manual refresh. That model works for many pages. It becomes less useful when information changes independently of the current user’s actions.

Common examples include:

  • New notifications
  • Order and shipment status
  • Support messages
  • Import and export progress
  • Live dashboards
  • Collaborative editing indicators
  • Queue and deployment activity

WebSockets keep a connection open between the browser and your application. When a Laravel event is broadcast, connected clients receive it immediately.

Laravel’s broadcasting system separates the event from the transport. Your application dispatches a normal event. Reverb sends it over WebSockets. Echo subscribes to the relevant channel in the browser.

Laravel Reverb sending live WebSocket events from a PHP backend to Vue browser windows

Install Reverb and enable broadcasting

For a current Laravel application, the fastest setup uses the install:broadcasting Artisan command:

php artisan install:broadcasting --reverb

The command installs Reverb, configures Laravel broadcasting, and adds the frontend dependencies and environment variables required by the scaffold.

If you prefer a manual installation, use:

composer require laravel/reverb
php artisan reverb:install

npm install --save-dev laravel-echo pusher-js

Reverb uses the Pusher protocol for its channels and messages. That is why pusher-js is part of the Echo setup.

Your local environment will contain values similar to these:

BROADCAST_CONNECTION=reverb

REVERB_APP_ID=local-app
REVERB_APP_KEY=local-key
REVERB_APP_SECRET=local-secret

REVERB_HOST=127.0.0.1
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 public REVERB_HOST and REVERB_PORT tell Laravel where to send broadcast messages. In production, these may point to a public WebSocket hostname behind a reverse proxy.

The Reverb server itself can listen on a different internal address. The Reverb configuration guide explains this distinction.

Start the required processes during development:

php artisan reverb:start
php artisan queue:work
npm run dev

Broadcast events use Laravel’s queue by default. Without a queue worker, the event may dispatch successfully but never reach the browser.

For production, Reverb is a long-running process. Use a process manager such as Supervisor, or use managed infrastructure such as Laravel Cloud. Reverb also supports horizontal scaling with Redis when one server cannot handle your connection count.

Configure Laravel Echo for Vue

The installer may add Echo configuration to your JavaScript bootstrap file. If you are configuring it yourself, create resources/js/echo.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'],
});

Import the file once from your Inertia entry point:

// resources/js/app.js

import './echo';

Restart Vite after changing environment variables. Vite exposes only variables with the VITE_ prefix to browser code.

Laravel Echo 1.16.0 or newer is required for the reverb broadcaster. Check the official Reverb broadcasting documentation if your existing scaffold uses an older Echo configuration.

Broadcast an event from Laravel

Let’s build a notification for a specific user. Private channels are the right choice because notifications should not be visible to every connected visitor.

Generate an event:

php artisan make:event NotificationCreated

Define the event in app/Events/NotificationCreated.php:

<?php

namespace App\Events;

use App\Models\User;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class NotificationCreated implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(
        public User $user,
        public string $message,
    ) {}

    public function broadcastOn(): array
    {
        return [
            new PrivateChannel('user.'.$this->user->id),
        ];
    }

    public function broadcastAs(): string
    {
        return 'notification.created';
    }

    public function broadcastWith(): array
    {
        return [
            'id' => str()->uuid()->toString(),
            'message' => $this->message,
            'created_at' => now()->toISOString(),
        ];
    }
}

ShouldBroadcast tells Laravel to send the event through the configured broadcaster. broadcastOn defines the channel. broadcastWith keeps the browser payload small and explicit.

Authorize the private channel in routes/channels.php:

<?php

use App\Models\User;
use Illuminate\Support\Facades\Broadcast;

Broadcast::channel('user.{userId}', function (User $user, int $userId) {
    return $user->id === $userId;
});

Now dispatch the event from a controller, service, job, or listener:

use App\Events\NotificationCreated;

NotificationCreated::dispatch(
    user: $user,
    message: 'Your export is ready.',
);

If the event is dispatched inside a database transaction, consider implementing ShouldDispatchAfterCommit. This prevents the queue from broadcasting data before the transaction has committed.

Listen in a Vue 3 component

A Vue component can subscribe when it mounts and leave the channel before it unmounts. This lifecycle cleanup matters in an Inertia application because page components can be created and destroyed during navigation.

<script setup>
import { onBeforeUnmount, onMounted, ref } from 'vue';
import { usePage } from '@inertiajs/vue3';

const page = usePage();
const notifications = ref([]);

const channelName = `user.${page.props.auth.user.id}`;

const addNotification = (notification) => {
    notifications.value.unshift(notification);
};

onMounted(() => {
    window.Echo
        .private(channelName)
        .listen('.notification.created', addNotification);
});

onBeforeUnmount(() => {
    window.Echo.leave(channelName);
});
</script>

<template>
    <section>
        <h2>Notifications</h2>

        <ul>
            <li
                v-for="notification in notifications"
                :key="notification.id"
            >
                {{ notification.message }}
            </li>
        </ul>
    </section>
</template>

The leading dot in .notification.created is important. It tells Echo that the event uses a custom broadcast name instead of the default event namespace.

Always leave channels when a component is destroyed. Otherwise, revisiting a page can create duplicate listeners. One server event may then add the same notification several times.

Current Laravel Echo packages also include Vue helpers such as useEcho through @laravel/echo-vue. Those helpers can manage subscription cleanup automatically. The explicit onMounted and onBeforeUnmount approach remains useful when you need custom subscription logic or want the lifecycle behavior to stay visible.

Vue 3 component receiving a live Laravel notification with lifecycle cleanup symbols

Combine WebSockets with Inertia props

WebSocket events and Inertia props solve different problems.

WebSockets are useful for announcing that something changed. Inertia props remain useful for retrieving the authoritative server state.

For example, the event can add a small notification immediately. You can then refresh the notifications prop:

import { router } from '@inertiajs/vue3';

const addNotification = (notification) => {
    notifications.value.unshift(notification);

    router.reload({
        only: ['notifications'],
        preserveScroll: true,
        preserveState: true,
    });
};

This pattern is useful when the event payload does not contain every field required by the interface. It also protects against stale local state.

Your controller might provide the initial data like this:

use Inertia\Inertia;

public function index()
{
    return Inertia::render('Dashboard', [
        'notifications' => fn () => auth()
            ->user()
            ->notifications()
            ->latest()
            ->limit(20)
            ->get(),
    ]);
}

Use the event payload directly when the update is small and complete. Reload selected props when the page needs fresh relationships, counts, permissions, or computed values.

This keeps Inertia’s server-driven model intact. Vue reacts instantly, while Laravel remains the source of truth.

usePoll versus WebSockets

WebSockets are not the only way to keep an Inertia page current. Inertia provides usePoll for periodic server requests:

<script setup>
import { usePoll } from '@inertiajs/vue3';

usePoll(5000, {
    only: ['stats'],
});
</script>

Polling is often the better choice when:

  • Updates can arrive several seconds late
  • The page has few active users
  • Infrastructure should remain simple
  • You already have a server-rendered endpoint
  • The feature does not justify a persistent connection

WebSockets are a better fit when:

  • Users expect immediate updates
  • Events are irregular or frequent
  • You need chat, presence, or collaboration
  • Repeated HTTP requests would create unnecessary load
  • The server already knows exactly when state changes

You can also combine both approaches. Use Reverb for fast updates, then use router.reload({ only: [...] }) for reconciliation. Keep a slower poll as a fallback if your product requires resilience during temporary WebSocket disconnects.

Bright comparison illustration showing Inertia polling with a clock beside Laravel Reverb WebSockets with a lightning path

Test the complete flow

Test the Laravel side with event fakes:

Event::fake();

NotificationCreated::dispatch($user, 'Your export is ready.');

Event::assertDispatched(NotificationCreated::class);

Then verify the browser flow:

  1. Start Reverb.
  2. Start a queue worker.
  3. Run the Vite development server.
  4. Open the Inertia page.
  5. Dispatch the event.
  6. Confirm the WebSocket connection in the browser tools.
  7. Confirm the notification appears once.
  8. Navigate away and back.
  9. Confirm the old listener does not receive duplicate events.

For debugging, run Reverb with its debug flag:

php artisan reverb:start --debug

You can monitor Reverb connections and messages with Laravel Pulse. In production, monitor queue failures and WebSocket process health as well.

Real-time functionality does not require abandoning Laravel’s conventions. A Laravel PHP web framework application can broadcast focused events, a Vue 3 component can react to them, and Inertia can continue delivering authoritative server-driven data.

That combination gives you live interfaces without recreating your entire application as a separate JavaScript system.

Previous
Structured Outputs in the Laravel AI SDK: Typed, Valid JSON From Any LLM
Next
The Inertia 3.x Data Playbook: Defer, Poll, or Fetch in Your Laravel + Vue SPA