Skip to main content
The MercureBroadcaster covered here was added in laravel/framework PR #61474 (merged September 10, 2026). At the time of writing it is not yet part of a tagged release and is not documented on the official docs site. This page is a preview based on the source code and docblocks — check the laravel/framework changelog before relying on it in production.

Overview

Laravel’s broadcasting layer has long shipped Reverb, Pusher, and Ably as “WebSocket-native” drivers. A new mercure 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 EventSource API — 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:
A sample connection configuration is provided as well:
If url is omitted, the driver falls back to FrankenPHP’s built-in Mercure hub (the mercure_publish() function). That decision happens in CreatesMercureDrivers::mercure():
When you run an external hub, 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. The CreatesMercureDrivers trait builds separate token factories for publishing (server → hub) and subscribing (browser → hub):
  • secret, publish_secret, and subscribe_secret can each be configured independently, falling back to secret.
  • algorithm, publish_algorithm, and subscribe_algorithm can likewise be set per side (default HS256).
  • HS256 requires a secret of at least 32 bytes, HS384 at least 48, and HS512 at least 64; an InvalidArgumentException is thrown otherwise.
The publish token carries Grant::ACTION_PUBLISH for every topic (*) and is memoized by CachingTokenProvider, so a fresh JWT isn’t minted on every single broadcast call. 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:
Crucially, a single denied channel does not fail the whole response — it’s flagged as 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 with private-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.
Subscribers decrypt in the browser using the 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)

When client_events is enabled (the default), every guarded channel is assigned a dedicated “whisper topic” that subscribers may publish to:
The channel’s own topic stays server-only, so a client can never forge a server-originated event. This mirrors Pusher’s “client events” feature, but keeps authorization cleanly separated by using distinct topics.

Topic naming

Because Mercure hubs are often shared across multiple applications, topics are namespaced under a topic_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:
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:
If you use a __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.
Last modified on September 13, 2026