Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Centrifugo Broadcaster Laravel Package

opekunov/laravel-centrifugo-broadcaster

Centrifugo broadcaster driver for Laravel that lets you broadcast real-time events over WebSockets through a Centrifugo server using Laravel's familiar broadcasting API. Simple to install and configure, it offers a drop-in way to add scalable real-time features to your app.

View on GitHub
Deep Wiki
Context7

Getting Started

What this package is: a Laravel broadcast driver for Centrifugo — a standalone Go websocket server. Laravel keeps doing what it always does (fire events, queue jobs, authorize channels), but the fan-out to browsers goes through Centrifugo's HTTP API instead of Pusher/Ably/Redis. You get self-hosted realtime with no per-connection vendor bill.

Install (v3.0.2):

composer require opekunov/laravel-centrifugo-broadcaster
php artisan vendor:publish --tag=centrifugo-config

Supported matrix is now Laravel 8.75 – 13.x on PHP 8.0 – 8.4. If you're on Laravel 13, this release is the one that declares and CI-verifies it (orchestra/testbench ^11, phpunit ^12); nothing else changed at the API level.

Wire up the three moving parts (this is where most of the first hour goes):

  1. .env — CENTRIFUGO_SECRET (must equal Centrifugo's token_hmac_secret_key), CENTRIFUGO_API_KEY (must equal Centrifugo's api_key), CENTRIFUGO_HOST/API URL and path. These are two different credentials — the secret signs JWTs, the API key authenticates server-side publishes.
  2. config/broadcasting.php — the package registers a centrifugo driver; point your default connection at it, or add a named connection and set BROADCAST_CONNECTION=centrifugo (BROADCAST_DRIVER pre-11).
  3. Centrifugo's own config.json — allowed origins, allow_subscribe_for_client, and channel namespaces. Laravel can't do this for you.

First use case — prove the loop end to end:

class OrderShipped implements ShouldBroadcast
{
    public function broadcastOn(): array
    {
        return [new Channel('orders')];
    }
}

// client
const centrifuge = new Centrifuge('ws://localhost:8000/connection/websocket');
centrifuge.subscribe('orders', ctx => console.log(ctx.data));
event(new OrderShipped($order)); // publish() hits Centrifugo's /api

Then immediately do the private-channel case (token endpoint + Broadcast::channel callback), because public channels need no auth and will hide all your credential mistakes until production.

Where to look first in the source: the service provider (how the driver is registered and what it binds), the broadcaster class's auth() / broadcast() methods, and the Centrifugo client wrapper. Those three files are the whole package; the rest is config and docs.

Implementation Patterns

Standard event broadcasting. Nothing package-specific: ShouldBroadcast events, Channel/PrivateChannel/PresenceChannel in broadcastOn(), broadcastAs() for a short event name (recommended — FQCNs in the wire payload are noisy and leak namespace structure), and broadcastWith() to trim the payload.

Token issuing is the piece you own. Centrifugo doesn't authenticate users against Laravel; it verifies a JWT you mint. Expose a small authenticated endpoint:

Route::post('/centrifugo/token', fn (Request $request) => [
    'token' => Centrifugo::generateToken((string) $request->user()->id),
]);

Then keep the TTL short (config token_ttl) and have the frontend re-fetch on centrifuge.on('disconnected') rather than trusting a long-lived token.

Channel authorization stays in Laravel. Broadcast::channel('orders.{orderId}', fn ($user, $orderId) => ...) is evaluated by the broadcaster's auth() path. Centrifugo's own allow_subscribe_for_client / namespace settings decide whether a client may subscribe at all; the Laravel callback decides whether this user may subscribe to this channel. Both layers apply — a permissive Centrifugo namespace plus a strict Laravel callback is fine; the reverse is a hole.

Publishing from outside an event. For jobs, commands, or webhook handlers where modelling an event is overkill, call the client directly:

Centrifugo::publish('orders.42', ['event' => 'status', 'data' => [...]]);

Wrap this in a small Realtime/CentrifugoPublisher service of your own so tests can swap it; don't sprinkle the facade through domain code.

Queue your broadcasts. Broadcasting goes over HTTP; a slow Centrifugo (restart, GC pause, network blip) in the request path is a latency tax. implements ShouldBroadcast + queue worker is the default; use ShouldBroadcastNow only for genuinely latency-critical paths, and give it a real timeout/retry policy.

Namespace your channels by domain. orders.{id}, users.{id}.notifications, presence.chat.{room}. Centrifugo namespaces let you tune history size, presence, and client-side publish per prefix — that's an ops lever you can't use if everything lives in flat channel names.

Multi-connection setups. If you're migrating from Pusher, keep both connections in config/broadcasting.php and flip per-event with broadcastVia() / per-environment via BROADCAST_CONNECTION. Roll out one feature at a time.

Testing. Fake at the event layer (Event::fake() + assert dispatched/broadcast), and for the client call itself either point CENTRIFUGO_HOST at a stub server or fake the underlying HTTP client if your installed client version routes through Laravel's HTTP client. Do not write tests that hit a live Centrifugo — that's exactly the flakiness class v3.0.2 removed from the package's own suite (they dropped httpstat.us for timeout/retry tests).

Gotchas and Tips

  • Two credentials, easy to cross. CENTRIFUGO_SECRET = token_hmac_secret_key (JWT signing, HS256). CENTRIFUGO_API_KEY = api_key (HTTP API header). The classic bug: token endpoint works, but publishes 401 — or vice versa. Verify by hitting /api with curl and decoding a token at jwt.io before blaming the package.
  • The JWT sub must be a string. Centrifugo rejects/complains on integer subjects. Cast it: (string) $user->id. Same string must be used everywhere you compare user ids in Broadcast::channel callbacks.
  • toOthers() won't exclude the sender. Pusher's protocol carries a socket_id that the server filters on; Centrifugo has no such socket id in the broadcast payload. The socket field travels through as ordinary data. If you need sender-exclusion, send the sender's id in the payload and filter client-side, or use a separate private channel for the originator.
  • API path and version drift. Centrifugo v3/v4/v5 differ in endpoints (/api), method names, and response envelopes; v5 also trimmed some legacy methods. Pin your Centrifugo image tag in Docker and match the package's configured API path to it. If publishes silently no-op, log the raw response body — the package surfaces errors unevenly across versions.
  • Channel namespaces change the wire name. A namespace prefix (chat, presence) is stripped/rewritten server-side; a private- prefix must align between your Laravel channel, the Broadcast::channel registration, and Centrifugo's namespace config. Mismatches manifest as "token valid, subscription denied with no obvious reason."
  • Token expiry is two-sided. The JWT exp and Centrifugo's client_expired_close_delay / client_stale_close_delay interact. Short tokens without a client refresh path = users silently going dark after an hour. Test a forced expiry locally.
  • HTTP client behaviour under failure. Publishes are fire-and-forget-ish; check whether your configured timeout/retry settings are actually applied and whether failures throw or return false. In a queued broadcast, swallowing means lost messages — add your own logging around the broadcaster if you need delivery guarantees, or enable Centrifugo history and reconcile.
  • Payload size. Centrifugo will happily relay multi-megabyte JSON until it doesn't (proxy limits, browser memory). Enforce a convention: broadcast identifiers and diffs, let the client fetch the rest. Cap broadcastWith() deliberately.
  • Behind a reverse proxy. Websocket upgrade needs Upgrade/Connection header pass-through and long proxy_read_timeout; the HTTP publish path needs the same TLS/host you configured. Half-broken setups look like "broadcasts work in staging but not through the load balancer."
  • Extension points if you need to diverge: publish the config and override token_ttl/API path; re-bind the client in a service provider if you need custom Guzzle options (proxies, mTLS); and remember the driver is registered via Broadcast::extend, so you can register a subclass under a second driver name rather than forking.
  • Upgrading to 3.0.2: no breaking changes over 3.0.1 — this is a compatibility/docs/CI release. A plain composer update is enough; the only reason to care is if you're targeting Laravel 13 and were blocked by the ^13.0 constraint. Take the opportunity to bump orchestra/testbench/phpunit in your own dev dependencies to match.
  • Code quality bar to copy: the release adds Pint + PHPStan and 100% coverage with deterministic timeout/retry tests. If you're writing an in-house driver or wrapper around this one, that combination (static analysis on the client wrapper, no external HTTP dependencies in tests) is the pattern worth stealing.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle