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 BroadcastsToTenant trait which provides:
    • tenantChannel(?string $suffix) — returns PrivateChannel('tenant.{id}') or PrivateChannel('tenant.{id}.{suffix}')
    • userChannel(int $userId) — returns PrivateChannel('user.{userId}')
    • globalChannel() — returns Channel('global')
    • broadcastWith() — automatically merges broadcastPayload() with _tenant_id and _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

  1. 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.

  2. Broadcast auth outside tenancy middleware/api/v1/broadcasting/auth queries the central tenant_users table. If tenancy middleware ran, it would switch to the tenant DB which doesn't have this table.

  3. Events broadcast via queueShouldBroadcast events go through the Redis queue (processed by Horizon), keeping request latency low.

  4. Dynamic import for SSR safety — Echo and Pusher use browser APIs (window, WebSocket). The EchoProvider uses import() to load the Echo module only on the client side.