Yes, as it 's "just" SSE, this works pretty well behind Cloudflare and other similar solutions. Mercure also automatically sends the NGINX-specific headers required to allow unbuffered connections.
Native mobile apps are also entirely supported. It's a very common use case. Most mobile languages have SSE client libraries.
That is if the connection isn't downgraded by some other mechanism. Lots of corporate clients downgrade connections to HTTP/1.1 due to how their network is setup.
There is also an issue with SSE I've noticed that websockets doesn't seem to have as much of a prevalent issue with, and that is being blocked by corporate firewalls.
Most websites use TLS, HSTS and the like nowadays. It’s hard for a corporate proxy to downgrade the connection.
Also, the limitation is only applied by browsers, not by (reverse) proxies and other non-browsers clients.
That being said, Mercure allow to subscribe to many "topics" using a single HTTP connection. This helps mitigating the limitation with HTTP/1 (which is very uncommon these days).
Indeed that's how it works. One of the key point of the solution is that you don't need anything client-side. The native EventSource class is all you need (but you can use more advanced SSE client libraries if wanted).
Reconnection and state reconciliation are achieved automatically. The hub implements all the needed features: it stores sent events and automatically resend them at reconnection time if they have been lost. It's possible to do this transparently because browsers will automatically send the ID of the last received message in a Last-Event-ID header when reconnecting. This feature is just often not implemented by SSE servers (because it’s not trivial to do).
It's also possible to ask events received since a specific ID, matching one or several topics just by passing the query parameters defined in the protocol section.
By the way, we are working on a new website that will make these things more clear.
Hi Kévin! A while ago, I’ve built a hub implementation using Typescript and Deno, mainly for learning, but also to see if I could come up with a neat solution for distributed event storage in-memory, using Raft. It implements the full spec, so far, and is pretty easy to understand, if I may say so myself.
Do you still accept entries for alternative hubs? It may be helpful for others to understand how the specification is supposed to work; some parts were a little opaque to me at first, and required digging into the reference implementation to fully grasp.
And it doesn't appear you have an associated working group (WG) for your (expired) draft publication, which could help you identify if you are reinventing an existing wheel..
WebSub cannot be used by browsers because browsers cannot receive POST requests. WebSub is for server to server communications.
Mercure is basically WebSub over SSE.
The draft has been discussed several times on the HTTP WG.
WebSockets are hard to secure (they totally bypass CORS as well as other browser built-in protections), don't work (yet) with HTTP/3 and for most use cases require to implement many features by yourself: reconnection in case of network failure, refetch of lost messages, authorization, topic mechanism…
Mercure, which is built on top of SSE (it's more an extension to SSE than an alternative to it) fix these issues.
However, SSE (as well as WebSockets) can be hard to use with stacks not designed to handle persistent connections such as PHP, serverless, most web servers proxying Ruby, Python etc apps. Even for languages designed to handle persistent connections, it's often more efficient and easier to manage persistent connections with ah-hoc software running on dedicated hardware.
That's what Mercure allows. Mercure provides a "hub", a server that will maintain the persistent connections with the browsers, store events, and re-send them in case of network issues (or if asked for old messages by a client). To broadcast a message to all connected users, the server app (or even a client) can just send a single POST request to the hub. The hub will also check that clients are authorized to subscribe or publish to a given topic (that's why JWT is used).
The reference implementation is written in Go, as a module for the Caddy web server, and his very efficient/optimized (it can handle thousands of persistent connections on very small machines).
Install a Mercure hub and you have all these features available without having to write any code. Client-side, no SDK is required, you can embrace the built-in EventSource JavaScript class.
You can reimplement the Same Origin Policy serverside by checking that the Origin header equals the Host header. Even more secure would be to check both against an allowlist (this protects against DNS rebinding, which the Same Origin Policy doesn't protect against).
>as well as other browser built-in protections
I'm curious what those are.
>for most use cases require to implement many features by yourself: [...] authorization
Isn't auth of websockets generally the same as auth of any Javascript-initiated HTTP request (e.g. fetch())? Check that the cookie looks good? Now, in the cause of OAuth tokens, websockets are more difficult than fetch(), because you cannot attach an Authorization: Bearer header to a websocket. But OAuth is less common than cookies for websites.
Once the connection is upgraded, you loose all metadata included in the HTTP headers (because it’s not HTTP) and all protections relying on it.
Also CORS and SOP can be bypassed: https://dev.to/pssingh21/websockets-bypassing-sop-cors-5ajm
Of course you can reimplement everything by hand (and you must if you use WebSockets), but with SSE/Mercure you don't have to because it's plain old HTTP.
> Once the connection is upgraded, you loose all metadata included in the HTTP headers (because it’s not HTTP) and all protections relying on it.
The Upgrade request is HTTP and you can extract all needed metadata from there and store it server side as needed. Those metadata wouldn't change during an active WebSocket session anyway, would they?
The auth headers (Authorization, Cookie) are all passed along, and that's what I want to establish a secure connection from the browser.
For more customized wishes there's always this "ticket"-based flow[0][1] that shouldn't be hard to implement. I might be a bit naive, but what needed metadata and custom headers are we talking about?
Like I said, you can reimplement SOP by checking that the Origin header equals the Host header. Or alternatively check them against an allowlist. It's just a couple lines of code, and then it's as secure as normal HTTP.
>The initial handshake occurs via HTTP Upgrade request, the response body is disregarded and the HTTP/HTTPS protocol is upgraded to WS/WSS protocol.
But the response body isn't disregarded. What could best be described as the response body is the server->client websocket messages. Those aren't disregarded.
Thanks, that's an interesting vulnerability. I'm not so sure it's websocket-specific though. It looks like the attack could be done without websockets at all. The end request that does the malicious action is plain HTTP. The initial request that triggers the desync uses a Connection: Upgrade header, which is used in websockets, but isn't actually websocket-specific. Connection: Upgrade was created in RFC 2616 from 1999, which was before the websocket RFC 6455 in 2011.
The vulnerability was there was a proxy, and the proxy didn't properly handle the Connection header as specified in RFC 2616. RFC 2616 says that a proxy must remove the Connection header if it's not handling it itself, and the proxy in this case wasn't handling it itself (not checking for 101 response).
I would say this vulnerability is an HTTP request smuggling vulnerability. That's a vulnerability where there's a proxy that implements some type of security, and the proxy can be tricked into sending a request to the backend that the proxy didn't actually parse. HTTP request smuggling vulnerabilities impact regular HTTP, not just websockets. This has nothing to do with the Same Origin Policy.
> WebSockets are hard to secure (they totally bypass CORS as well as other browser built-in protections), don't work (yet) with HTTP/3 and for most use cases require to implement many features by yourself: reconnection in case of network failure, refetch of lost messages, authorization, topic mechanism…
Having written WebSocket CORS with Authentication (Cognito) I know this isn't quite true. The initial connection is a standard HTTP request that returns a 100 series, with preflights and everything. That initial request has all the headers you might want to sent to a server for auth. It's a bit odd IMO but the auth string is sent in the web socket constructor, second argument, really easy to miss. Happy to provide both server and client code examples.
You might consider updating the readme to make it clear what Mercure/SSE does that WebSockets doesn't.
If it was me, I'd also put in the readme what WebSockets does that Mercure/SSE does not, such as bidirectional communication, low latency client-to-server messages, binary data transfer, etc.
Exactly this, the landing page seem much more about the reference implementation than the actual protocol. I will say the docs at https://mercure.rocks/spec are nice.
I have been using Server Sent Events with PHP since 2017 and it works very well. All you need is the right headers and a loop that breaks on user disconnect. Honestly it was super easy and had no issues setting it up and getting it to work.
It might be worth mentioning to the parent poster that SSEs (and Mercure) are unidirectional only (server -> client), whereas WebSockets are bidirectional.
Nowadays that's hardly an issue because SSE is multiplexed over HTTP/2 and HTTP/3 alongside regular requests. In some ways, SSE is today more efficient than websockets too because if you use it with HTTP/3 then you're avoiding head of line blocking which is still an issue with websockets.
(Also in practice many places websockets are not even supporting HTTP/2 because on the server they were implemented on top of raw sockets which is no longer possible with HTTP/2)
Head-of-line still exists in HTTP/3, but on another level. A stream can still get blocked. With websockets the whole connection gets blocked (HTTP/1), but that connection isn't shared with anything else, right?
Worth noting that HTTP/3 seems to recover faster than TCP-based protocols.
A while ago I created Mercure: an open pub-sub protocol built on top of SSE that is a replacement for WebSockets-based solutions such as Pusher. Mercure is now used by hundreds of apps in production.
At the core of Mercure is the hub. It is a standalone component that maintains persistent SSE (HTTP) connections to the clients, and it exposes a very simple HTTP API that server apps and clients can use to publish. POSTed updates are broadcasted to all connected clients using SSE. This makes SSE usable even with technologies not able to maintain persistent connections such as PHP and many serverless providers.
Mercure also adds nice features to SSE such as a JWT-based authorization mechanism, the ability to subscribe to several topics using a single connection, events history, automatic state reconciliation in case of network issue…
I maintain an open-source hub written in Go (technically, a module for the Caddy web server) and a SaaS version is also available.
Wow, it's fascinating how a single HN comment can drive meaningful traffic to a project! I'm the author of Centrifugo, and I appreciate you mentioning it here.
Let me share a bit more about Centrifugo transport choices. It’s not just about supporting multiple transports — developers can also choose between bidirectional and unidirectional communication models, depending on their needs.
For scenarios where stable subscriptions are required without sending data from the client to the server, Centrifugo seamlessly supports unidirectional transports like SSE, HTTP-streaming, unidirectional gRPC streams, and even unidirectional WebSockets (this may sound kinda funny for many I guess). This means integration is possible without relying on client-side SDKs.
However, Centrifugo truly shines in its bidirectional communication capabilities. Its primary transport is WebSocket – with JSON or Protobuf protocols, with SSE/HTTP-streaming fallbacks that are also bidirectional — an approach reminiscent of SockJS, but with more efficient implementation and no mandatory sticky sessions. Sticky sessions is an optimization in Centrifugo, not a requirement. It's worth noting that SSE only supports JSON format, since binary is not possible with it. This is where HTTP-streaming in conjuction with ReadableStream browser API can make much more sense!
I believe Centrifugo gives developers the flexibility to choose the transport and communication style that best fits their application's needs. And it scales good out of the box to many nodes – with the help of Redis or Nats brokers. Of course this all comes with limitations every abstraction brings.
It comes down to all the extra bytes sent and processed (local and remote, and in flight) by long polling. SSE events are small while other methods might require multiple packets and all the needless headers throughout the stack, for example.
I've used Mercure before at a startup of mine. Self hosted. Worked great! And still works to this day (I haven't actively worked on that startup for years myself.)
This script is entirely optional and is only used to create static builds of FrankenPHP or to create self-executable PHP apps. It doesn't require a Docker image but does need some system dependencies.
It is not necessary to create a standard (dynamically linked) build of FrankenPHP and isn't used in the Docker images we provide: https://frankenphp.dev/docs/compile/
Optionally, you can embed the PHP scripts in the final binary using a Go feature similar to `include_bytes`: https://frankenphp.dev/docs/embed/
But for PHP, it's even simpler: PHP is available as a C library, and we just use it. (libphp can be embedded using static compilation, or used as a dynamic library, we support both).
> But for PHP, it's even simpler: PHP is available as a C library, and we just use it. (libphp can be embedded using static compilation, or used as a dynamic library, we support both).
Yes, but that's completely independent from whether / how you embed the scripts? (Which you do via the the procedure described in your first link?)
Btw, how does your version of PHP compare to Facebook's Hack? I had opportunity to use Hack a few years ago, and I think it's the best language they could have made (from PHP as the fixed starting point).
This means that in addition to any PSR-7 implementation, you can run apps using HttpFoundation (Symfony, Laravel, Drupal, etc) and "legacy" code using superglobals without the need for an adapter.
Unlike Laravel and Symfony, WordPress doesn't support the worker mode of FrankenPHP (yet?), so there are not many benefits in terms of performance (except the ability to preload assets using 103 Early Hints, which can reduce the latency of a page load by 30%).
That being said, FrankenPHP makes it easy to enable HTTP cache with WordPress and simplifies the deployment story. There is a dedicated project for WordPress and FrankenPHP, that comes with a built-in HTTP cache tailored for WordPress (using the Souin Go library): https://github.com/StephenMiracle/frankenwp
> Unlike Laravel and Symfony, WordPress doesn't support the worker mode of FrankenPHP (yet?), so there are not many benefits in terms of performance (except the ability to preload assets using 103 Early Hints, which can reduce the latency of a page load by 30%).
This part is clear for me, but thank you for mentioning HTTP 103 too. I will not state for sure, but in my blurry memory, FPHP (FrankenPHP) was _slower_ than Apache+mod_php in that tests. But again, I won't say for sure, I just remember I was totally impressed as was expecting otherwise - much likely some subtle differences in setup on my side. If/when I have more precise info - I may ping you.
> That being said, FrankenPHP makes it easy to enable HTTP cache with WordPress and simplifies the deployment story. There is a dedicated project for WordPress and FrankenPHP, that comes with a built-in HTTP cache tailored for WordPress (using the Souin Go library): https://github.com/StephenMiracle/frankenwp
Thank you, have not seen that yet - may get idea or two from it. At glance, they just do naive `BYPASS_PATH_PREFIX` handling and that's all.
Beyond tests, I of course do prefer Nginx over Caddy and "simplifies the deployment story" doesn't resonate with my needs much yet - one of that things may change of course.
Native mobile apps are also entirely supported. It's a very common use case. Most mobile languages have SSE client libraries.