How to Add Realtime to a Laravel App
Add WebSocket-powered realtime features to your Laravel application — publishing events from controllers and subscribing from the frontend with a few lines of code.
Laravel has always had a clean story for broadcasting events over WebSockets. The traditional path runs through Laravel Echo paired with Pusher: you fire a ShouldBroadcast event, Laravel's queue system delivers it to Pusher's servers, and Echo listens on the frontend. It works well, but it ties you to a third-party service with its own pricing tiers and regional constraints.
Apinator is a managed WebSocket infrastructure platform that fits into the same slot. It speaks the same channel-based protocol, has a PHP server SDK for signing and publishing events, and a JavaScript client SDK for the browser. You do not need to rework your Laravel application architecture — just swap the publishing and subscription layers.
This post walks through adding realtime order status updates to a Laravel e-commerce application using Apinator.
Installing the PHP SDK
Add the SDK to your project with Composer:
composer require apinator/apinator-php
The SDK has no external dependencies. It uses PHP's built-in hash_hmac for request signing and curl for HTTP. It requires PHP 8.1 or later.
Configuring Credentials
Store your Apinator credentials in .env:
APINATOR_APP_ID=your_app_id
APINATOR_API_KEY=your_api_key
APINATOR_API_SECRET=your_api_secret
Add a binding in config/services.php or wire it up directly wherever you initialize the client. A clean approach is to bind it as a singleton in a service provider:
// app/Providers/AppServiceProvider.php
use Apinator\RealtimeClient;
public function register(): void
{
$this->app->singleton(RealtimeClient::class, function () {
return new RealtimeClient(
appId: env('APINATOR_APP_ID'),
apiKey: env('APINATOR_API_KEY'),
apiSecret: env('APINATOR_API_SECRET'),
);
});
}
You can now inject RealtimeClient into any controller, job, or service class through Laravel's container.
Publishing an Event from a Controller
The most direct way to broadcast is to call publish after completing a database write. Here is an order status update triggered from a controller action:
// app/Http/Controllers/OrderController.php
namespace App\Http\Controllers;
use App\Models\Order;
use Apinator\RealtimeClient;
use Illuminate\Http\Request;
class OrderController extends Controller
{
public function updateStatus(Request $request, Order $order, RealtimeClient $apinator): \Illuminate\Http\JsonResponse
{
$validated = $request->validate([
'status' => ['required', 'string', 'in:processing,shipped,delivered,cancelled'],
]);
$order->update(['status' => $validated['status']]);
$apinator->publish(
channel: "private-order.{$order->id}",
event: 'order.status_updated',
data: [
'order_id' => $order->id,
'status' => $order->status,
'updated_at' => $order->updated_at->toIso8601String(),
],
);
return response()->json(['ok' => true]);
}
}
The channel name uses a private- prefix, which tells Apinator that subscribers must be authenticated before they can connect. The event name is arbitrary — the frontend will filter on it.
Publishing Asynchronously with Laravel Jobs
Calling publish inline in a controller is fine for low-traffic endpoints, but for anything heavier — or when you want the HTTP response to return before the broadcast round-trip completes — move the publish into a queued job.
// app/Jobs/BroadcastOrderStatusUpdated.php
namespace App\Jobs;
use Apinator\RealtimeClient;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
class BroadcastOrderStatusUpdated implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
public function __construct(
public readonly int $orderId,
public readonly string $status,
public readonly string $updatedAt,
) {}
public function handle(RealtimeClient $apinator): void
{
$apinator->publish(
channel: "private-order.{$this->orderId}",
event: 'order.status_updated',
data: [
'order_id' => $this->orderId,
'status' => $this->status,
'updated_at' => $this->updatedAt,
],
);
}
}
Dispatch it from the controller after saving:
$order->update(['status' => $validated['status']]);
BroadcastOrderStatusUpdated::dispatch(
orderId: $order->id,
status: $order->status,
updatedAt: $order->updated_at->toIso8601String(),
)->onQueue('broadcasts');
This keeps the controller lean and lets your queue workers handle retries if the Apinator API is temporarily unreachable.
Channel Authentication Endpoint
Private channels require subscribers to prove they are allowed to listen. When the JS client connects to a private channel, it makes a POST request to your backend with a socket_id and channel_name. Your backend verifies the user's session and, if authorized, returns a signed token.
Register the route in routes/api.php:
Route::post('/broadcasting/auth', [BroadcastingController::class, 'auth'])
->middleware('auth:sanctum');
The controller method checks that the authenticated user actually owns the order before signing the channel token:
// app/Http/Controllers/BroadcastingController.php
namespace App\Http\Controllers;
use App\Models\Order;
use Apinator\RealtimeClient;
use Illuminate\Http\Request;
class BroadcastingController extends Controller
{
public function auth(Request $request, RealtimeClient $apinator): \Illuminate\Http\JsonResponse
{
$socketId = $request->input('socket_id');
$channelName = $request->input('channel_name');
// Extract the order ID from the channel name (e.g. "private-order.42")
$orderId = (int) str($channelName)->afterLast('.');
$order = Order::findOrFail($orderId);
// Only the order's owner may subscribe
if ($request->user()->id !== $order->user_id) {
return response()->json(['message' => 'Forbidden'], 403);
}
$auth = $apinator->authorizeChannel($socketId, $channelName);
return response()->json($auth);
}
}
authorizeChannel computes the HMAC-SHA256 signature over {socket_id}:{channel_name} using your API secret and returns the auth string the client needs to complete the handshake.
Subscribing on the Frontend
Install the JavaScript client SDK:
npm install @apinator/sdk
Initialize the client, point it at your auth endpoint, and subscribe to the order's private channel:
// resources/js/realtime.js
import { RealtimeClient } from '@apinator/sdk';
const apinator = new RealtimeClient({
appId: import.meta.env.VITE_APINATOR_APP_ID,
authEndpoint: '/broadcasting/auth',
// authEndpoint receives the CSRF token automatically via fetch credentials
});
export function watchOrderStatus(orderId, onUpdate) {
const channel = apinator.subscribe(`private-order.${orderId}`);
channel.bind('order.status_updated', (data) => {
onUpdate(data);
});
return () => {
apinator.unsubscribe(`private-order.${orderId}`);
};
}
Wire this into your page or component. In a plain Blade view you might call it directly:
import { watchOrderStatus } from './realtime';
const orderId = window.__ORDER_ID__; // set by Blade
const unsubscribe = watchOrderStatus(orderId, ({ status }) => {
document.getElementById('order-status').textContent = status;
});
// Clean up when the user navigates away
window.addEventListener('beforeunload', unsubscribe);
If you are using a frontend framework like Vue or React the pattern is the same — call watchOrderStatus inside a mounted / useEffect hook and call the returned cleanup function on unmount.
Putting It Together
The full flow for an order status update looks like this:
- A warehouse operator hits the admin panel, triggering
OrderController@updateStatus. - Laravel saves the new status to the database and dispatches
BroadcastOrderStatusUpdatedto the queue. - A queue worker picks up the job and calls
$apinator->publish(...), sending the event to Apinator's infrastructure. - Apinator fans the event out to all WebSocket connections subscribed to
private-order.{id}. - The customer's browser receives the event and updates the status label instantly — no polling required.
Because the publish happens in a queued job, the admin panel response is fast regardless of network conditions between your servers and Apinator. If the publish fails, Laravel's queue will retry it according to your configured backoff policy.
What to Add Next
Once the basic channel is working, a few natural extensions present themselves. Presence channels (prefixed with presence-) let you track which support agents or customers are actively viewing an order. Client events let customers send typing indicators or confirmations back through the channel without an extra HTTP round-trip. And because Apinator handles connection state, you do not need to manage WebSocket infrastructure, sticky sessions, or horizontal scaling concerns on your own servers.
The PHP SDK's publish method accepts an array of channels in a single call, which is useful when an order update needs to notify both the customer's channel and an internal ops dashboard channel simultaneously.
Check the Apinator documentation for the full PHP SDK reference, including webhook verification if you want to react to connection and disconnection events server-side.