springboardvr/laravel-ably-broadcaster
An Ably broadcaster driver for Laravel that plugs Ably’s realtime messaging into Laravel’s broadcasting system, enabling events to be sent to public, private, and presence channels with minimal configuration. Add scalable realtime features to your PHP app quickly and reliably.
Install and pin. The repo is archived and the last tag is 1.2.0 (Sep 2020, Laravel 8 support only), so pin rather than float:
composer require springboardvr/laravel-ably-broadcaster:^1.2
composer why springboardvr/laravel-ably-broadcaster # confirm what resolved
If auto-discovery is enabled, the service provider registers the ably driver with BroadcastManager. Check composer.json → extra.laravel.providers and confirm the class appears in bootstrap/cache/packages.php after php artisan package:discover.
Add a connection in config/broadcasting.php — this is the only config the package needs:
'connections' => [
'ably' => [
'driver' => 'ably',
'key' => env('ABLY_KEY'), // server-side API key, never VITE_/EXPO_PUBLIC_
],
// ...existing pusher/redis/log entries
],
Switch the default. On a Laravel 8-era app this is BROADCAST_DRIVER=ably in .env; if you have since moved to Laravel 10.17+/11 this key is BROADCAST_CONNECTION. Then php artisan config:clear.
Look first at your existing ShouldBroadcast events, not at this package. The driver is drop-in — nothing in your event classes, controllers, or broadcastOn() / broadcastWith() methods changes. Inventory what you already broadcast (grep -r ShouldBroadcast app/) and pick the least critical one.
First use case: take one existing private-channel event (e.g. OrderShipped on PrivateChannel('orders.'.$order->id)), flip BROADCAST_DRIVER=ably, fire it from php artisan tinker (broadcast(new OrderShipped($order))), and watch the Ably Dev Console. You should see a message land on the channel with the event name from broadcastAs(). That single round-trip validates Composer resolution, provider discovery, config caching, outbound HTTPS to Ably's REST endpoint, and channel naming — in that order of likely failure.
Reuse the events you already have. Nothing about the driver changes the Laravel side. Keep writing what you already write:
class OrderShipped implements ShouldBroadcast
{
public function __construct(public Order $order) {}
public function broadcastOn(): array
{
return [new PrivateChannel('orders.'.$this->order->id)];
}
public function broadcastAs(): string
{
return 'order.shipped'; // short name — much easier to match client-side
}
public function broadcastWith(): array
{
return ['id' => $this->order->id, 'status' => $this->order->status]; // small payload
}
}
Always set broadcastAs(). The default event name is the FQCN (App\Events\OrderShipped), which is awkward to subscribe to from Ably's own JS client and easy to get wrong through Echo's namespace mangling. A short dotted name (order.shipped) removes an entire class of silent-mismatch bugs.
Choose ShouldBroadcastNow deliberately. Plain ShouldBroadcast is queued — a broken Ably key or unreachable network surfaces as a failed job minutes later, not a 500. For UI-critical events (auth handshakes, toOthers() notifications) use ShouldBroadcastNow; for high-volume feeds stay queued and monitor failed jobs.
Private/presence auth runs through /broadcasting/auth. Keep your routes/channels.php callbacks untouched:
Broadcast::channel('orders.{orderId}', fn (User $u, int $orderId) =>
$u->id === Order::find($orderId)?->user_id
);
Verify BroadcastServiceProvider (or your equivalent) is registered so the /broadcasting/auth route exists. The client hits that endpoint exactly as it did for Pusher.
Echo on the client. Laravel Echo has no native Ably adapter; the standard pattern is Echo's Pusher driver pointed at Ably's Pusher-compatible endpoint, with authEndpoint unchanged:
window.Echo = new Echo({
broadcaster: 'pusher',
key: import.meta.env.VITE_ABLY_KEY, // Ably *public* key or token-request key
wsHost: 'realtime-pusher.ably.io',
wsPort: 443,
forceTLS: true,
encrypted: true,
disableStats: true,
authEndpoint: '/broadcasting/auth',
// namespace: false, // <-- see Gotchas
});
Echo.private(`orders.${orderId}`).listen('.order.shipped', handler);
If you use Ably's native JS client instead, skip Echo entirely and subscribe with the exact channel + event names the driver emits (channel.subscribe('order.shipped', cb)), obtaining an Ably token from the same /broadcasting/auth flow.
Hide the driver behind one class. Even though it's drop-in, put a thin adapter between your domain and broadcast():
final class RealtimePublisher
{
public function orderShipped(Order $o): void { broadcast(new OrderShipped($o)); }
public function orderShippedNow(Order $o): void { broadcast_now(new OrderShipped($o)); }
}
When you fork the archived package or swap to ably/ably-php behind a custom Broadcast::extend('ably', ...) driver (very likely within a year given the archive status), one class changes.
Notifications use the same driver. Notification::send() with the broadcast channel routes through BROADCAST_DRIVER; nothing extra to wire. Just ensure each notification's toBroadcast() payload stays small.
auth() / validAuthenticationResponse()) is the most likely breakage point. Write the adapter class now so the replacement is a one-file change.private- prefix in Echo/Pusher semantics; verify what the driver actually sends to Ably (subscribe in the Dev Console and compare) and subscribe client-side with the exact same string. Mismatch produces no error — just no message. Presence channels have the same trap with presence-.App.Events and silently prefixes listened event names. Combined with a dotted broadcastAs() name and Echo's leading-dot alias (.order.shipped = "don't prefix"), this is a recipe for missed events. Either set namespace: false or always use the leading dot.broadcastWith() returning an Eloquent model with relations will blow this quickly. Always return IDs + scalars; let the client re-fetch via your API. A too-large payload fails on the Ably side and, for queued events, only shows up as a failed job.toOthers() is unreliable across Pusher-compat bridges because it requires a socket ID the driver may not receive from Ably. Test it explicitly; if it's broken, fall back to putting the sender's ID in the payload and filtering client-side.storage/logs/laravel.log and the failed-jobs table for 24h — a bad key doesn't fail the HTTP request for ShouldBroadcast.ShouldBroadcastNow with a blocked egress is a hard 500; that's actually a good canary for day one.config/broadcasting.php or .env: php artisan config:clear locally, then php artisan config:cache in staging/prod. A stale broadcasting.php cache is the most common "it worked on my machine" report.Event::fake() asserts the event dispatched, never that Ably received anything — it won't catch driver regressions. For that, bind a fake Broadcaster ($app->instance(BroadcastManager::class, $fake)) or mock AblyBroadcaster::broadcast(), and keep one opt-in integration test hitting a sandbox Ably app.bootstrap/cache/packages.php → php artisan about shows the connection → config not cached → broadcast(new TestEvent) from tinker succeeds → message visible in Ably Dev Console → client subscribed to the exact channel/event string. Stop at the first failing step; don't re-check later ones.vendor/ edits: Broadcast::extend('ably', fn ($app, $config) => ...) in your own AppServiceProvider lets you override or replace the driver without forking — do this before the package's own registration if you need to shadow it. Avoid subclassing the package's broadcaster class; its internals are the least stable surface and won't survive a fork.How can I help you explore Laravel packages today?