fresh/centrifugo-bundle
PHP bundle that integrates the Centrifugo real-time messaging server into Symfony apps. Configure connections, issue JWT client tokens, publish messages to channels, and add websocket-powered live updates, notifications, and presence features with minimal setup.
fresh/centrifugo-bundle is a thin Symfony integration for Centrifugo: it registers the framework-agnostic centrifugo/centrifugo-php HTTP client as a service and feeds it your API URL, API key, and JWT secret. That's essentially the whole package — the value is configuration plus DI wiring, not new behaviour.
A Laravel developer should be upfront about the fit: this will not install "usefully" into a Laravel app. There is no auto-discovery, no service provider, no Facade. You can use the underlying client directly — and that's what you should do — while treating the bundle as a reference implementation for config keys and token semantics. Reading DependencyInjection/Configuration.php, DependencyInjection/*Extension.php, and the service definitions tells you everything the bundle does in about five minutes.
Minimal path in a Symfony app (the intended use):
api_key and a token HMAC secret configured; they must match your app config.composer require fresh/centrifugo-bundle.config/packages/fresh_centrifugo.yaml with api_url (include the /api path), api_key, and secret.publish().The Laravel equivalent (same client, same semantics):
// AppServiceProvider::register()
$this->app->singleton(\Centrifugo\Centrifugo::class, fn () => new \Centrifugo\Centrifugo(
config('services.centrifugo.api_url'), // e.g. http://centrifugo:8000/api
config('services.centrifugo.api_key'),
config('services.centrifugo.secret'),
));
First use case worth building: a GET /realtime/token endpoint that returns a connection JWT for the authenticated user, plus a single publish call after a domain event (order.updated, message.created). That covers 80% of real Centrifugo usage.
Config + client, nothing more. Keep the bundle's key names (api_url, api_key, secret) in your Laravel config/services.php so the mental model and docs map 1:1 if you ever move between frameworks.
Connection tokens at the edge. Generate the JWT in a controller after auth, not in the model layer:
$token = $client->generateConnectionToken((string) $user->id, time() + 3600, [
'name' => $user->name,
]);
return response()->json(['token' => $token]);
sub should be a string. Pass info as a JSON string in the underlying client (it's embedded in the token, not the payload).
Subscription tokens for private channels. Public channels need nothing; private ones need a JWT scoped to channel + sub. Endpoint per channel (or a whitelist check) is the standard pattern:
$client->generateSubscriptionToken((string) $user->id, "user:{$user->id}", time() + 3600);
Channel naming is the architecture. Adopt a convention early: user:{id} for per-user fanout, chat:{roomId}, presence:{tenant}. Configure matching namespaces in Centrifugo (namespaces with presence, history_size, force_recovery) rather than inventing ad-hoc names — recovery/history only work if the namespace is configured server-side.
Publish from events/listeners, after commit. The single most common bug is publishing inside a DB transaction that later rolls back, leaving clients with ghost state. Use DB::afterCommit() (or a queued listener) and publish from there.
$client->publish([
'channel' => "user:{$order->user_id}",
'data' => ['type' => 'order.updated', 'id' => $order->id, 'status' => $order->status],
]);
Prefer broadcast over N publishes when the same payload goes to many channels — one HTTP round-trip instead of dozens. For per-user fanout to thousands of users, loop broadcast in chunks, not publish per user.
Wrap it behind your own interface. Define RealtimePublisher with publish(), broadcast(), token(). Bind the Centrifugo client behind it. This gives you a fake for tests, a seam for swapping providers, and insulation from the bundle's (low) release cadence.
Testing. Bind a fake/mock of your interface in the container and assert the payload shape rather than the HTTP call. If you test the client itself, point api_url at a stub HTTP server and assert X-API-Key plus JSON body.
Frontend pairing. Use centrifuge-js v5: new Centrifuge(url, { getToken }) where getToken calls your token endpoint; subscriptions get their own getToken for private channels. Tokens expire — the client will re-request, so your endpoint must be cheap and idempotent.
api_key ≠ secret. The API key authenticates HTTP API calls (X-API-Key header). The secret signs JWTs. Mixing them up produces "unauthorized" on publish and "invalid token" on connect, which look similar but have different causes. Both must match Centrifugo's config exactly.token_hmac_secret_key; v5+ nests it under client.token.hmac_secret_key. Confirm against the server version you're running before debugging your PHP side.api_url must include /api. The client appends the method path; a missing /api yields 404s that look like auth failures. Also avoid a trailing slash.->error on the result rather than an exception. Check it explicitly, or wrap the client so it throws — otherwise failures are silent.sub is a string, and info is a JSON string. Passing an int ID or an array for info produces tokens that Centrifugo rejects at connect time, not at generation time. Cast and json_encode at the boundary.allowed_channels/namespaces. A publish to an undefined channel can fail or silently vanish depending on version — configure namespaces first, then name channels.services.centrifugo values are frozen at config:cache; restart workers after env changes. Don't call the client in a service provider's boot() (network on every request). Use a singleton binding and resolve lazily.curl -X POST http://centrifugo:8000/api -H "X-API-Key: ..." -d '{"method":"publish","params":{"channel":"test","data":{}}}' isolates server/config problems from PHP problems in one step. Check Centrifugo logs for rejected tokens before touching application code.How can I help you explore Laravel packages today?