Real-time WebSocket broadcasting with Laravel Reverb and Laravel Echo.
Vito Business OS uses Laravel Reverb as the WebSocket server and Laravel Echo as the browser client. Events broadcast from the server are received by connected clients instantly, without polling.
Broadcast events are queued — they never block the HTTP response. The queue worker (queue:work) deserializes the event and forwards it to Reverb, which fans it out to all subscribers on the target channel.
All channel names and their authorization logic live in a single file — ChannelRegistry — to keep naming consistent across events and authorization rules.
Channel pattern
Access
Purpose
tenant.{id}
Any authenticated user belonging to the tenant
Order events (paid, cancelled, shipped, refunded)
tenant.{id}.admins
Users with role seller or super_admin
Low-stock alerts, new login security alerts
user.{id}
The user themselves
Personal notifications (login alert, profile update)
use App\Broadcasting\ChannelRegistry;use Illuminate\Support\Facades\Broadcast;// Tenant channel — any member of the tenantBroadcast::channel(ChannelRegistry::TENANT_CHANNEL_PATTERN, function ($user, $id) { return $user->tenant_id === (int) $id;});// Admin-only channel — seller or super_admin onlyBroadcast::channel(ChannelRegistry::TENANT_ADMIN_CHANNEL_PATTERN, function ($user, $id) { if ($user->tenant_id !== (int) $id) { return false; } return in_array($user->role, ['seller', 'super_admin'], true);});// User channel — the user themselvesBroadcast::channel(ChannelRegistry::USER_CHANNEL_PATTERN, function ($user, $id) { return $user->id === (int) $id;});// Order channel — the customer who placed the orderBroadcast::channel(ChannelRegistry::ORDER_CHANNEL_PATTERN, function ($user, $id) { $customerId = \App\Models\Order::where('id', $id) ->where('tenant_id', $user->tenant_id) ->value('customer_id'); return $customerId !== null && $user->id === $customerId;});
Never reference channel name strings directly in event classes. Always use ChannelRegistry::tenant(), ChannelRegistry::tenantAdmins(), ChannelRegistry::user(), or ChannelRegistry::order(). This ensures the channel name in the event and the authorization rule always match.
All broadcast events set $tries = 1 and $timeout = 30 seconds, and expire after 60 seconds via retryUntil(). Stale events are discarded rather than retried.
BROADCAST_CONNECTION=reverbREVERB_APP_ID=your-app-idREVERB_APP_KEY=your-app-keyREVERB_APP_SECRET=your-app-secretREVERB_HOST=localhostREVERB_PORT=8080REVERB_SCHEME=http# Exposed to Vite / frontendVITE_REVERB_APP_KEY="${REVERB_APP_KEY}"VITE_REVERB_HOST="${REVERB_HOST}"VITE_REVERB_PORT="${REVERB_PORT}"VITE_REVERB_SCHEME="${REVERB_SCHEME}"
For production, change REVERB_PORT to 443, REVERB_SCHEME to https, and set REVERB_HOST to your domain.
Reverb uses the Pusher wire protocol. The broadcaster: 'reverb' option and pusher-js dependency are both required even though you are not using Pusher’s hosted service.
Event names registered with broadcastAs() must be prefixed with a . when passed to .listen(). The dot tells Echo the name is already fully qualified and should not be namespaced.
Listening to the admin-only channel for security alerts:
window.Echo .private(`tenant.${tenantId}.admins`) .listen('.security.login.new', (data) => { showAlert(`New login from ${data.ip_address} on ${data.device}`); });
In production, Nginx terminates TLS and proxies WebSocket connections to Reverb on port 8080. Supervisor keeps both the Reverb process and queue workers running as daemons.
Place this in /etc/supervisor/conf.d/laravel-reverb.conf. Replace [APP_PATH] and [USER] with your actual values (deployment/supervisor/laravel-reverb.conf):