jestays / laravel-centrifugo
Laravel broadcasting driver for Centrifugo 6+ with multi-application channel and user scoping.
Requires
- php: ^8.2
- ext-json: *
- centrifugal/phpcent: ^6.0
- laravel/framework: ^11.0|^12.0|^13.0
Requires (Dev)
- laravel/pint: ^1.13
- orchestra/testbench: ^9.0|^10.0|^11.0
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^10.5|^11.5|^12.0
README
A Laravel broadcasting driver for Centrifugo 6+, built on top of the official
centrifugal/phpcent client.
This package is designed for a single Centrifugo server shared by several Laravel applications. It scopes channels and user identities per application, so different applications can safely share the same Centrifugo instance without colliding.
Architecture
Laravel
|
jestays/laravel-centrifugo
|
centrifugal/phpcent
|
Centrifugo
Laravel remains responsible for authentication, subscription authorization, and publishing. Transports (WebSocket, SSE, HTTP streaming) are entirely Centrifugo's responsibility; this package does not implement any transport and does not depend on Laravel Echo.
Installation
composer require jestays/laravel-centrifugo php artisan centrifugo:install
The installer publishes config/centrifugo.php, adds the required environment variables to .env, registers
the centrifugo broadcasting connection, and sets BROADCAST_CONNECTION=centrifugo.
Configuration
BROADCAST_CONNECTION=centrifugo CENTRIFUGO_URL=http://localhost:8000 CENTRIFUGO_API_KEY= CENTRIFUGO_TOKEN_HMAC_SECRET_KEY= CENTRIFUGO_APP=pos CENTRIFUGO_TOKEN_TTL=3600 CENTRIFUGO_VERIFY=true
CENTRIFUGO_APP identifies the current application on the shared Centrifugo server. It is required and must
match [a-z0-9_-]+. The package fails with a clear error as soon as a channel or user mapper is resolved
without a valid CENTRIFUGO_APP.
CENTRIFUGO_TOKEN_TTL is the default time-to-live, in seconds, applied to connection and subscription tokens
issued through TokenManager, the Centrifugo service, and the token endpoints when no explicit TTL is
given. A TTL of 0 produces a token with no exp claim, i.e. a token that never expires; use that
deliberately. Negative TTL values are rejected with an InvalidArgumentException.
CENTRIFUGO_VERIFY controls TLS certificate verification for the phpcent HTTP client when talking to
CENTRIFUGO_URL. Keep it true in production; only disable it for local development against a
self-signed Centrifugo instance.
BROADCAST_CONNECTION=centrifugo must be the default broadcasting connection. Broadcast::channel()
authorization callbacks are registered on Laravel's default broadcasting connection, and the subscription
token endpoint resolves that same default connection to reuse them. If BROADCAST_CONNECTION is set to
anything other than centrifugo, the subscription token endpoint throws a clear RuntimeException instead
of silently failing.
Broadcasting example
Business code keeps using the standard Laravel broadcasting primitives:
final class OrderUpdated implements ShouldBroadcast { public function __construct(private readonly Order $order) { } public function broadcastOn(): array { return [ new PrivateChannel("orders.{$this->order->id}"), ]; } }
With CENTRIFUGO_APP=pos, this event is published to Centrifugo as:
private:pos.orders.123
The event never needs to know about Centrifugo namespaces or application scoping.
Channel naming
Every Centrifugo channel follows the same structure:
<namespace>:<application>.<channel>
| Laravel channel | Centrifugo channel |
|---|---|
new Channel('stock.updated') |
public:pos.stock.updated |
new PrivateChannel('user.123') |
private:pos.user.123 |
new PresenceChannel('branch.10') |
presence:pos.branch.10 |
Namespace names (public, private, presence) are configurable in config/centrifugo.php, but there is a
single, modern naming strategy: legacy $channel naming is not supported.
Name restrictions
CENTRIFUGO_APPmust match[a-z0-9_-]+, e.g.pos,proplus,qms.- The Laravel channel name (the part after
private-/presence-, or the whole name for public channels) must match[A-Za-z0-9@,;._=-]+. Names such asorders.123,users.123.notifications,branch-10, andstock_updatedare all valid. - Centrifugo's reserved symbols (
:,#,$,/,*,&) and whitespace are rejected. This keeps<namespace>:<application>.<channel>unambiguous and reversible when the subscription token endpoint maps a Centrifugo channel back to its Laravel name.
Invalid application names throw an InvalidArgumentException when the mappers are resolved; invalid channel
names throw Jestays\Centrifugo\Exceptions\InvalidCentrifugoChannel as soon as a channel is mapped.
Multi-application scoping
The same Centrifugo server can serve multiple Laravel applications, each with its own CENTRIFUGO_APP:
private:pos.orders.123
private:proplus.orders.123
private:qms.orders.123
An application can never request a subscription token for a channel that belongs to another application.
ScopedChannelMapper rejects any channel whose application segment does not match the current
CENTRIFUGO_APP.
User identity
Authenticated Laravel users are mapped to Centrifugo user identifiers scoped by application, so the same Laravel user ID never collides across applications:
POS user 123 -> pos:123
Pro+ user 123 -> proplus:123
Centrifugo server namespace configuration
Centrifugo 6 expects channel namespaces under the top-level channel key. Configure matching namespaces on
the server side:
{
"channel": {
"namespaces": [
{
"name": "public",
"allow_subscribe_for_client": true
},
{
"name": "private"
},
{
"name": "presence",
"presence": true
}
]
}
}
The same configuration as an environment variable (useful for Docker):
CENTRIFUGO_CHANNEL_NAMESPACES='[{"name":"public","allow_subscribe_for_client":true},{"name":"private"},{"name":"presence","presence":true}]'
Subscriptions to private: and presence: channels always require a subscription token issued through your
Laravel Broadcast::channel() authorization callbacks.
What public means
public does not mean "anyone on the Internet can subscribe". Connecting to Centrifugo still requires a
valid connection token issued to an authenticated Laravel user, and allow_subscribe_for_client: true only
lets those already-authenticated, non-anonymous connections subscribe to public:* channels without a
subscription token.
Two consequences worth knowing:
- Public channels bypass Laravel's
Broadcast::channel()authorization entirely. - On a shared Centrifugo server, connection tokens from every application are signed with the same secret,
so an authenticated
proplususer can subscribe topublic:pos.stock.updated. Only publish data to public channels that any authenticated user of any application on the server may read; useprivate:orpresence:channels otherwise.
Do not enable allow_subscribe_for_anonymous on these namespaces: this package's design assumes every
connection belongs to an authenticated user.
Token endpoints
The package optionally registers two routes (enabled by default, see config/centrifugo.php):
| Method | Route | Purpose |
|---|---|---|
| POST | /centrifugo/connection-token |
Issues a Centrifugo connection token |
| POST | /centrifugo/subscription-token |
Issues a Centrifugo subscription token |
The subscription token endpoint expects a channel field containing the Centrifugo channel name (e.g.
private:pos.orders.123). It maps the channel back to its Laravel name (orders.123) and runs it through
your normal Broadcast::channel() authorization callbacks before issuing a token.
Routes configuration
Routes are controlled by config('centrifugo.routes'):
'routes' => [ 'enabled' => true, 'prefix' => 'centrifugo', 'middleware' => ['web', 'auth'], ],
enabledtoggles route registration entirely; set it tofalseif you want to expose the endpoints yourself, or not use them at all.prefixis the URL prefix under which both endpoints are registered (/{prefix}/connection-tokenand/{prefix}/subscription-token).middlewareis fully configurable and not tied to any specific guard. Use['api', 'auth:sanctum']for a stateless SPA/mobile client, or keep thewebsession guard for a first-party web app. Partial overrides inconfig/centrifugo.php(e.g. only settingprefix) do not remove the other defaults; missing keys always fall back toenabled: true,prefix: 'centrifugo',middleware: ['web', 'auth'].
Frontend usage
Clients connect directly to Centrifugo using the official centrifuge-js
SDK, using the two token endpoints above as getToken callbacks. With the default web/auth middleware,
the endpoints sit behind Laravel's session guard and CSRF protection, so requests must send the
XSRF-TOKEN cookie back as an X-XSRF-TOKEN header and include credentials:
import { Centrifuge } from 'centrifuge'; function xsrfToken() { return decodeURIComponent(document.cookie.match(/XSRF-TOKEN=([^;]+)/)?.[1] ?? ''); } async function fetchToken(url, body) { const response = await fetch(url, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', 'X-XSRF-TOKEN': xsrfToken(), }, body: body ? JSON.stringify(body) : undefined, }); const { token } = await response.json(); return token; } const centrifuge = new Centrifuge('ws://localhost:8000/connection/websocket', { getToken: () => fetchToken('/centrifugo/connection-token'), }); const subscription = centrifuge.newSubscription('private:pos.orders.123', { getToken: () => fetchToken('/centrifugo/subscription-token', { channel: 'private:pos.orders.123' }), }); centrifuge.connect(); subscription.subscribe();
If your frontend is a stateless SPA or mobile client instead, set centrifugo.routes.middleware to
['api', 'auth:sanctum'] (Sanctum token/PAT authentication) and drop the CSRF/cookie handling above in
favour of a plain Authorization: Bearer <token> header.
Centrifugo service / facade
For explicit server-to-server calls, inject Jestays\Centrifugo\Centrifugo or use the Centrifugo facade.
It accepts Laravel-style channel names and raw user IDs, and maps them internally, so business code never
constructs a Centrifugo channel or user identity by hand:
use Jestays\Centrifugo\Facades\Centrifugo; Centrifugo::publish('orders.123', ['status' => 'shipped']); Centrifugo::broadcast(['orders.123', 'stock.updated'], ['status' => 'shipped']); Centrifugo::presence('branch.10'); Centrifugo::presenceStats('branch.10'); Centrifugo::history('orders.123', limit: 10); Centrifugo::historyRemove('orders.123'); Centrifugo::subscribe('orders.123', $userId); Centrifugo::unsubscribe('orders.123', $userId); Centrifugo::disconnect($userId); Centrifugo::channels(); Centrifugo::info(); Centrifugo::connectionToken($user); Centrifugo::subscriptionToken($user, 'orders.123');
Centrifugo::client() is an escape hatch that returns the underlying \phpcent\Client instance for anything
this service does not wrap.
Every method above throws Jestays\Centrifugo\Exceptions\CentrifugoApiError when Centrifugo responds with an
HTTP 200 that still carries a top-level error key (Centrifugo's API-level error shape, e.g. an unknown
channel or namespace) — callers never receive a silent error array.
channels() and info() are server-wide operations authenticated purely by CENTRIFUGO_API_KEY. They are
not application-scoped: on a shared Centrifugo server, channels() returns channels for every
application, not just the current one. Filter the result yourself (e.g. by the <namespace>:<application>.
prefix) if you need application-scoped results.
Transports
This package does not implement WebSocket, SSE, or HTTP streaming. Those transports are Centrifugo's responsibility; clients can use any official Centrifugo SDK to connect over the transport of their choice.
Legacy channels
This package does not support legacy $ private channels.
Credits
This package was originally based on denis660/laravel-centrifugo
and is now independently maintained and versioned by jestays. It relies on the
official centrifugal/phpcent client and targets
Centrifugo 6+.
License
Released under the MIT License.