Real-Time & Broadcasting
AutoCom uses Laravel Reverb as its WebSocket server and Laravel Echo on the frontend for real-time, bidirectional communication. Events are broadcast through Redis queues (processed by Horizon) and delivered to connected clients over WebSocket channels.
Architecture Overview
Backend Event (e.g. order created)
│
▼
ShouldBroadcast event dispatched
│
▼
Redis queue (processed by Horizon)
│
▼
Reverb WebSocket server
│
▼
Private channel: user.{id} / tenant.{id}
│
▼
Frontend Echo client receives event
│
▼
React context updates UI in real-time
Stack
| Component | Technology | Role |
|---|---|---|
| WebSocket server | Laravel Reverb | Pusher-protocol WebSocket server on port 8080 |
| Queue processor | Laravel Horizon | Processes BroadcastEvent jobs from Redis |
| Backend client | Laravel Broadcasting | Dispatches events to channels |
| Frontend client | Laravel Echo + pusher-js | Subscribes to channels, receives events |
| Transport | Redis | Queue backend for broadcast jobs |
Channel Architecture
AutoCom defines four channel types, all authorized via the central tenant_users table:
| Channel Pattern | Type | Purpose | Auth Check |
|---|---|---|---|
tenant.{tenantId} |
Private | Tenant-wide events (all members) | $user->belongsToTenant($tenantId) |
tenant.{tenantId}.{module} |
Private | Module-scoped events (e.g. workflows, orders) | $user->belongsToTenant($tenantId) |
user.{userId} |
Private | Personal events (notifications) | $user->id === $userId |
presence-tenant.{tenantId} |
Presence | Who's online in a tenant | $user->belongsToTenant($tenantId) |
Channel authorization is defined in routes/channels.php and runs on the central database — the /api/v1/broadcasting/auth endpoint is registered outside InitializeTenancyByRequestData middleware so it can query the tenant_users pivot table directly.
Configuration
Backend
config/broadcasting.php — Sets Reverb as the default broadcaster:
'default' => env('BROADCAST_CONNECTION', 'reverb'),
config/reverb.php — Reverb server configuration (host, port, app credentials, Redis scaling).
Environment variables (.env):
BROADCAST_CONNECTION=reverb
REVERB_APP_ID=autocom
REVERB_APP_KEY=your-app-key
REVERB_APP_SECRET=your-app-secret
REVERB_HOST=0.0.0.0
REVERB_PORT=8080
REVERB_SCHEME=http
Frontend
Environment variables (baked at build time via NEXT_PUBLIC_*):
NEXT_PUBLIC_REVERB_APP_KEY=your-app-key
NEXT_PUBLIC_REVERB_HOST=localhost
NEXT_PUBLIC_REVERB_PORT=5352
NEXT_PUBLIC_REVERB_SCHEME=http
The Echo client is created in frontend/lib/echo.ts and managed by the EchoProvider context (frontend/contexts/echo-context.tsx), which:
- Connects on authentication (token + tenant available)
- Reconnects when the tenant context switches (super admin switching tenants)
- Disconnects on logout
- Uses dynamic import to avoid SSR issues
Creating Broadcast Events
All tenant-aware events should extend TenantBroadcastEvent:
use App\Broadcasting\TenantBroadcastEvent;
class OrderShipped extends TenantBroadcastEvent
{
public function __construct(public Order $order) {}
public function broadcastOn(): array
{
return [$this->tenantChannel('orders')];
}
public function broadcastAs(): string
{
return 'order.shipped';
}
protected function broadcastPayload(): array
{
return [
'order_id' => $this->order->id,
'tracking_number' => $this->order->tracking_number,
];
}
}
The TenantBroadcastEvent base class (app/Broadcasting/TenantBroadcastEvent.php):
- Implements
ShouldBroadcast— events are queued, not sent synchronously - Uses the
BroadcastsToTenanttrait which provides:tenantChannel(?string $suffix)— returnsPrivateChannel('tenant.{id}')orPrivateChannel('tenant.{id}.{suffix}')userChannel(int $userId)— returnsPrivateChannel('user.{userId}')globalChannel()— returnsChannel('global')broadcastWith()— automatically mergesbroadcastPayload()with_tenant_idand_timestamp
Listening on the Frontend
useTenantChannel
Subscribe to tenant-scoped or module-scoped events:
import { useTenantChannel } from "@/hooks/use-channel";
// Listen to all tenant events
useTenantChannel(".order.shipped", (data) => {
console.log("Order shipped:", data.order_id);
});
// Listen to module-scoped events
useTenantChannel(".workflow.completed", (data) => {
console.log("Workflow done:", data.execution_id);
}, "workflows");
useUserChannel
Subscribe to personal events (notifications, etc.):
import { useUserChannel } from "@/hooks/use-channel";
useUserChannel(".notification.created", (data) => {
console.log("New notification:", data.notification.title);
});
useTenantPresence
Track who's online in a tenant:
import { useTenantPresence } from "@/hooks/use-channel";
useTenantPresence({
onHere: (members) => setOnlineMembers(members),
onJoining: (member) => addMember(member),
onLeaving: (member) => removeMember(member),
});
Note: Event names use a leading
.(e.g.,.order.shipped) to skip Laravel's default event namespace prefix.
Module Extensibility
Modules can register custom broadcast channels programmatically via the ChannelRegistry singleton:
// In your module's ServiceProvider boot() method:
use App\Broadcasting\ChannelRegistry;
$registry = app(ChannelRegistry::class);
$registry->registerTenantChannel('inventory', function ($user, $tenantId) {
return $user->belongsToTenant($tenantId)
&& $user->hasPermission('products.manage_inventory');
});
Or modules can simply call Broadcast::channel() directly in their service provider.
Docker Setup
Services
| Container | Role | Port |
|---|---|---|
autocom-reverb |
Reverb WebSocket server | 8080 (internal), 5352 (mapped) |
autocom-horizon |
Queue processor for broadcast events | — |
autocom-nginx |
Proxies /app path to Reverb for WebSocket upgrade |
5350 |
Nginx WebSocket Proxy
Nginx proxies WebSocket connections at the /app path (Reverb uses the Pusher protocol endpoint /app/{key}):
location /app {
proxy_pass http://reverb:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600;
proxy_send_timeout 3600;
}
Development
The composer dev script starts Reverb alongside other services:
composer dev # Starts: server, queue, logs, vite, reverb
Key Design Decisions
-
No BroadcastTenancyBootstrapper — Tenant isolation is enforced at the channel naming + authorization level, not by prefixing the broadcast driver. A single Reverb server serves all tenants.
-
Broadcast auth outside tenancy middleware —
/api/v1/broadcasting/authqueries the centraltenant_userstable. If tenancy middleware ran, it would switch to the tenant DB which doesn't have this table. -
Events broadcast via queue —
ShouldBroadcastevents go through the Redis queue (processed by Horizon), keeping request latency low. -
Dynamic import for SSR safety — Echo and Pusher use browser APIs (
window,WebSocket). TheEchoProviderusesimport()to load the Echo module only on the client side.