Overview
Laravel’s broadcasting layer has long shipped Reverb, Pusher, and Ably as “WebSocket-native” drivers. A newmercure driver value has now been added to config/broadcasting.php.
Mercure is a real-time protocol built on Server-Sent Events (SSE) rather than a bidirectional WebSocket connection. It behaves more like HTTP/1.1 or HTTP/2 long-polling, which gives it a few distinct properties:
- It passes through ordinary HTTP infrastructure (reverse proxies, CDNs, load balancers) transparently.
- Clients can be implemented with nothing more than the browser’s
EventSourceAPI — no dedicated client library required. - FrankenPHP ships a built-in Mercure hub, so no extra infrastructure is needed to try it.
The new driver value
The supported-drivers comment at the top of config/broadcasting.php now lists mercure:
url is omitted, the driver falls back to FrankenPHP’s built-in Mercure hub (the mercure_publish() function). That decision happens in CreatesMercureDrivers::mercure():
url should point to the management API used for publishing, while public_url is the URL the browser connects to. Splitting the two supports setups where publishing happens over an internal network (e.g. Docker Compose) while only the public URL is exposed to the browser.
Separate publish and subscribe tokens
Mercure is a JWT-driven access control protocol. TheCreatesMercureDrivers trait builds separate token factories for publishing (server → hub) and subscribing (browser → hub):
secret,publish_secret, andsubscribe_secretcan each be configured independently, falling back tosecret.algorithm,publish_algorithm, andsubscribe_algorithmcan likewise be set per side (defaultHS256).- HS256 requires a secret of at least 32 bytes, HS384 at least 48, and HS512 at least 64; an
InvalidArgumentExceptionis thrown otherwise.
Grant::ACTION_PUBLISH for every topic (*) and is memoized by CachingTokenProvider, so a fresh JWT isn’t minted on every single broadcast call.
Channel authorization via a single cookie
The biggest architectural difference from the WebSocket drivers is that subscriber authorization is handled through one cookie.MercureBroadcaster::auth() accepts a channel_names array (capped at 100 per request), evaluates authorization per channel, and then issues a single authorization cookie covering all of them:
denied: true in the response payload while the rest of the authorized channels keep working. This matters because Mercure multiplexes many topics over one EventSource connection, so channels can be added or removed mid-session without tearing down the connection.
Presence channels get a different Grant shape than regular private/private-encrypted channels, targeting the subscribe URL pattern used by Mercure’s Subscription API.
End-to-end encrypted channels
Channels prefixed withprivate-encrypted- are treated as end-to-end encrypted (E2EE) — meaning even the Mercure hub itself never sees the payload. This is a capability the existing Pusher and Reverb drivers don’t offer.
Setting a base64-encoded 32-byte encryption_key activates the ChannelEncrypter, which wraps the event name, payload, and socket ID into a JWE (JSON Web Encryption) before broadcast() sends it. The hub only ever relays encrypted bytes.
jwk (JSON Web Key) field returned by the auth() response — an out-of-band key exchange recommended by the Mercure specification, so the hub never learns the key.
Presence channels cannot be encrypted, since their member list flows through the hub’s Subscription API by design — that’s incompatible with E2EE.
Whispers (direct client-to-client messages)
Whenclient_events is enabled (the default), every guarded channel is assigned a dedicated “whisper topic” that subscribers may publish to:
Topic naming
Because Mercure hubs are often shared across multiple applications, topics are namespaced under atopic_prefix to avoid collisions. The default is:
.alt is a reserved, unresolvable DNS suffix (RFC 9476), so it can never collide with a real domain. Channel names are URL-encoded per RFC 3986 into a single path segment:
Cookie domain and error messages
Since a Mercure hub often runs on a different subdomain than the application (e.g.mercure.example.com vs. app.example.com), a failure to resolve a shared cookie domain throws a descriptive exception:
__Secure- or __Host- prefixed cookie name, the driver also verifies at boot time that public_url is HTTPS:
Choosing between Reverb, Pusher/Ably, and Mercure
Mercure is a good fit when you don’t need a persistent bidirectional connection — notifications, progress updates, or chat-like use cases that are primarily server-to-client. If you’re already running FrankenPHP, it works with no additional infrastructure at all.
Related pages
- Broadcasting basics
- Laravel Cloud (FrankenPHP-based runtime)