A new post lands and everyone else’s list stays stale until they refresh. Reverb pushes it to every open page. You need three moving parts running (queue, Reverb, Vite) and one composable; here they are.
TL;DR
php artisan install:broadcasting --reverb, then restart Vite.- Run a queue worker and
php artisan reverb:start; broadcasts are queued. - Implement
ShouldBroadcast, return aChannel, dispatch after the row exists. - Listen with
useEchoPublic('posts', 'PostPublished', ...);useEchois for private channels.
Polling the posts list every few seconds works until it does not. Reverb is Laravel’s WebSocket server. Echo is the client. A Vue page can subscribe with useEchoPublic from @laravel/echo-vue and update when a PHP event fires.
The official write-up is the broadcasting docs. This page is the short path for a Vue starter kit.
- Install broadcasting with Reverb.
- Run the queue worker and Reverb.
- Broadcast an event.
- Listen on the Vue page.
Install#
php artisan install:broadcasting --reverbThat installs Reverb, the Echo packages, and the VITE_REVERB_* keys. Restart Vite after it changes .env, or the browser will still see empty keys.
Broadcasts are queued. The setup guide starts a queue worker with composer run dev. If you are not using that script, run php artisan queue:work yourself. Then start the socket server:
php artisan reverb:startGotcha: two processes, two silent failures
Leave both running. An event with no worker sits in the jobs table. An event with no Reverb process never reaches the browser.
The event#
php artisan make:event PostPublishednamespace App\Events;
use App\Models\Post;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class PostPublished implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public Post $post) {}
public function broadcastOn(): Channel
{
return new Channel('posts');
}
}ShouldBroadcast is the line that sends it. A public Channel needs no auth. Use PrivateChannel when only a logged-in user should hear it, and authorize that name in routes/channels.php.
Dispatch it after the row exists:
PostPublished::dispatch($post);Heads up: public properties are public
Put that in store after Post::create(). The queued job serializes the model. Keep the payload small. Echo will hand the public properties to the browser, so hide anything you would not print on the page.
The Vue page#
<script setup lang="ts">
import { ref } from "vue";
import { useEchoPublic } from "@laravel/echo-vue";
const titles = ref<string[]>([]);
useEchoPublic(
"posts",
"PostPublished",
(event: { post: { title: string } }) => {
titles.value.unshift(event.post.title);
},
);
</script>
<template>
<ul>
<li v-for="title in titles" :key="title">{{ title }}</li>
</ul>
</template>Listens on private-posts, misses the event, then Matches the public Channel:
useEcho("posts", "PostPublished", onPublished);useEchoPublic("posts", "PostPublished", onPublished);useEchoPublic subscribes to the public channel when the component is set up and leaves it when the component goes away. useEcho is the private-channel hook, so it would listen on private-posts and miss this event. The event name matches the class name, PostPublished, unless you set broadcastAs().
Two windows
Create a post from another window. The title should appear without a reload. If it does not, check the queue worker, then Reverb, then the browser console for a failed WebSocket. A missing VITE_REVERB_APP_KEY fails quietly until you look.
Private channels and a separate SPA#
An Inertia app on the same origin can use the defaults install:broadcasting wrote. A Vue SPA on another port is the Sanctum case. Private channels then authorize through /api/broadcasting/auth, and Echo needs a custom authorizer that uses the credentialed Axios instance. The setup is in the Sanctum broadcasting section and depends on the cookie login from the Sanctum guide.
Gotcha
Do not point that authorizer at a bearer token you stored for your own SPA. Fix the cookie first.
Production#
Reverb is a process, like the queue worker. Laravel Cloud can run it as a managed WebSocket cluster and injects the REVERB_* variables. On a VPS you run php artisan reverb:start under a supervisor and proxy the socket port. The Vue bundle only needs the VITE_REVERB_* values from the production env at build time. The deploy guide covers the asset build. It does not start Reverb for you.
Recap#
- Reverb is the server, Echo the client, the queue the courier.
- Restart Vite after
.envchanges. - Public channel ↔
useEchoPublic; private channel ↔useEcho+channels.php. - In production, Reverb is another supervised process.
FAQ#
Why does my Laravel Reverb event never reach Vue?
Check, in order: a queue worker is running, reverb:start is running, and the browser has a VITE_REVERB_APP_KEY (restart Vite after .env changes).
What is the difference between useEcho and useEchoPublic?
useEchoPublic listens on a public channel. useEcho listens on a private channel, so it subscribes to private-… and misses public events.
Do broadcasts need a queue worker?
Yes. ShouldBroadcast events are queued; without a worker they wait in the jobs table.
Next steps#
- Queue a broadcast instead of blocking the save
- Laravel database notifications on a Vue page
- Deploy a Laravel + Vue app



