Laravel Reverb runs the WebSocket connection that lets a Laravel application push updates to a browser. For a private progress screen, success means the right person receives a useful signal and everyone else is denied. A working connection alone proves neither.

Consider a fictional report builder. An authenticated customer starts a report, a queued worker prepares it, and the page changes from “processing” to “ready”. This guide separates subscription permission, committed report state and event delivery. The PHP example targets Laravel 12; check your own framework and package lockfiles before installation. It is not a claim that Laravel 12 is the newest release.

Decide whether the screen needs WebSockets

If the user opens a report once an hour, a refresh button or bounded polling may be enough. Reverb becomes more useful when people stay on the screen and need changes promptly: a support queue, import progress or a dispatch board. Define an acceptable delay and expected number of open screens before choosing infrastructure. Polling a cheap status endpoint every few seconds may meet a small workflow's needs.

Choose the delivery mechanism for the workflow
NeedStarting pointTrade-off to examine
Occasional status checkRefresh button or pollingRequest frequency, backoff and the delay a user can accept.
Live activity in an open pageReverb with Laravel broadcasting and EchoLong-lived connections, access checks and reconnect recovery.
Business action must eventually happenDurable job or recorded operationA socket event does not replace processing or reconciliation.
Notify someone who is offlineA persistent notification and a delivery channelDo not depend on an open browser tab.

The three paths are separate: the browser connects to Reverb; private-channel authorization goes to the Laravel application; a Laravel broadcast job sends the event to Reverb. The Reverb documentation explains the transport and server lifecycle. The broadcasting documentation covers channel authorization and events.

Establish the connection and queue before the UI

In a disposable Laravel application with working authentication, run the documented installer and review its dependency, route, environment and JavaScript changes. Keep a saved baseline so installation does not silently replace existing setup.

php artisan install:broadcasting
php artisan route:list --path=broadcasting
php artisan channel:list

Choose Reverb when prompted, complete the generated Echo setup, and use BROADCAST_CONNECTION=reverb. For asynchronous broadcasting, configure a real queue connection and its storage. The event below uses the broadcasts queue; start a worker that listens to it as well as the Reverb process:

php artisan reverb:start
php artisan queue:work --queue=broadcasts

Run those in separate terminals for the exercise. The installed package's generated configuration is the starting point. The server's bind address, Laravel's destination for publishing events and the browser's reachable WebSocket hostname can differ, especially in containers or behind TLS. A browser cannot connect to a private Compose service name. Keep the Reverb app secret server-side; only public client configuration belongs in Vite variables.

Authorize one report, not just a logged-in user

For this example, reports has an integer id, an owning user_id, a status and an integer revision. The sample is single-owner: sharing, team roles and administrative access require a different explicit policy. Add this callback to routes/channels.php:

<?php

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

Broadcast::channel('reports.{reportId}', function (User $user, string $reportId): bool {
    if (! preg_match('/\A[1-9][0-9]*\z/', $reportId)) {
        return false;
    }

    return DB::table('reports')
        ->where('id', $reportId)
        ->where('user_id', $user->getAuthIdentifier())
        ->exists();
});

An authenticated owner can subscribe; a different user cannot. Rejecting malformed IDs also makes the lookup predictable. Apply the same ownership rule to the HTTP endpoint serving report details and downloads. A secret-looking report URL or the channel's private- prefix is not an access rule.

Origin restrictions answer which web origins may connect. They do not decide which report a person can read. Keep both controls, and test the denied cases using real channel authorization. If access is revoked after subscription, the existing socket may outlive that change: define how to disconnect it or move subsequent events to a newly authorized channel. Rechecking only when the browser happens to reconnect is insufficient for sensitive payloads.

Publish a small event after the transaction commits

Create app/Events/ReportStatusChanged.php. The explicit payload contains an ID, state and revision; it does not serialize the entire customer or report model.

<?php

namespace App\Events;

use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;
use Illuminate\Foundation\Events\Dispatchable;

class ReportStatusChanged implements ShouldBroadcast, ShouldDispatchAfterCommit
{
    use Dispatchable;

    public string $queue = 'broadcasts';

    public function __construct(
        private readonly int $reportId,
        private readonly string $status,
        private readonly int $revision,
    ) {}

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

    public function broadcastAs(): string
    {
        return 'report.status.updated';
    }

    public function broadcastWith(): array
    {
        return [
            'report_id' => $this->reportId,
            'status' => $this->status,
            'revision' => $this->revision,
        ];
    }
}

The report-writing service should increment revision with the state change inside its database transaction, then dispatch this event using the saved values. ShouldDispatchAfterCommit defers dispatch until that transaction commits; a rollback must not announce a completed report. The same service must authorize the operation and handle concurrent updates. Do not expose an endpoint that accepts arbitrary report IDs and “ready” states from the browser.

A commit followed by a process crash can still leave a missing notification. For this screen, the status endpoint is authoritative and the browser can recover by reading it. If the event represents a business action that must be delivered, record that intent durably and reconcile it; after-commit timing alone does not close every failure window.

Reconnect by reading current state

With Echo already initialized by the installer, the listener name includes a leading dot because this event uses broadcastAs(). The following is an integration fragment: refreshReport must be your authenticated HTTP refresh function, with loading and error handling.

const channel = window.Echo.private(`reports.${reportId}`);
channel.listen('.report.status.updated', () => refreshReport());

Read the current report after subscription succeeds, after a reconnect, and when the user explicitly refreshes. This closes the gap between the original page request and subscribing. Use the server's revision to prevent an older HTTP response replacing a newer screen state; cancel or sequence overlapping refreshes. Treat an event as a reason to refresh, not proof that an operation happened exactly once. Unsubscribe when leaving the report screen.

Find the failing boundary

Diagnose the report update path
ObservationInspect firstUseful evidence
Socket never connectsBrowser destination, TLS and proxy upgrade supportBrowser connection error plus proxy/Reverb logs.
Socket connects, authorization failsSession/guard, auth route and owner lookupAuthorization response and the requested report/channel ID.
Authorization succeeds, no event arrivesBroadcast connection, queue name and workerA queued job, its attempt/result and Reverb receiving the publish.
Event arrives, page stays oldCustom event name and refresh responseEcho listener name, HTTP status and report revision.
Only some updates disappearReconnects, worker failures and stale releasesCommitted database state compared with queue/process history.

Our Horizon guide explains queue operation when using Redis. It does not replace Reverb's connection process. Under load, measure connection count, message rate and payload size separately; a single optimistic concurrency number cannot describe all three.

Use a repeatable acceptance exercise

Download the private-channel example and test procedure. The local PHP checks exercise the owner/other-user/guest boundary through Laravel's Pusher-compatible broadcaster, verify the event's exact payload and queue, and check dispatch after commit versus rollback. They do not run a Reverb server, browser session, TLS proxy or load test. Those deployment checks are listed separately in the download.

For a handover, ask for a demonstration where the owner sees the report complete, another account is denied, the worker is temporarily stopped, and the browser reconnects after completion. Record what happens at each step. Our Sail guide covers local service addresses; our Laravel development service starts with the specific workflow and its acceptance conditions.