# dunglas/mercure: a self-hosted hub for SSE and WebSocket push

> Mercure is a protocol and a Go reference implementation for pushing resource updates to browsers and HTTP clients over Server-Sent Events. This review covers what the hub does, how to run it, and where the AGPL licence and the thin install docs become the deciding factor.

**dunglas/mercure** — 🪽 An open, easy, fast, reliable and battery-efficient solution for real-time communications

- Repository: https://github.com/dunglas/mercure
- Website: https://mercure.rocks
- Stars: 5,334 · Forks: 349
- Language: Go
- License: AGPL-3.0
- Published: 2026-09-22 · Updated: 2026-09-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/dunglas-mercure

## What dunglas/mercure actually is, and who it is for

The repository contains two things that are easy to conflate. The first is a protocol specification for pushing data updates to web browsers and other HTTP clients, maintained in this repository and also published as an Internet-Draft. The second is a reference, production-grade implementation of a Mercure hub, the server side of that protocol, written in Go and released under AGPL-3.0. The README describes the target use case directly: publishing async and real-time updates of resources served through web APIs, to reactive web and mobile apps.

That framing matters for who should care. This is not a general-purpose message broker. It is a way to attach live updates to resources that already have URLs. If your application already exposes a REST or hypermedia API and clients already fetch resources from it, Mercure gives those clients a channel to learn that a resource changed, without polling. The topics in the protocol are IRIs, which is consistent with that hypermedia orientation; the repository also lists hypermedia and async-api among its topics.

The repository ships more than the server. There is a Go library that can be used in any Go application to implement the Mercure protocol directly, without a hub. That is a genuinely different deployment shape from running the hub as a separate process, and it is the option most readers skip past. There is also an official Docker image and a managed, high-scalability version of the hub sold separately on mercure.rocks. The README is explicit that the managed version exists, so the free software path and the paid path are both first-class options from the maintainer's point of view.

## The mechanism: publishers, subscribers and topic IRIs

The protocol has two roles. A publisher sends an update to the hub. A subscriber holds an open connection to the hub and receives updates for the topics it subscribed to. The README's subscriptions diagram and the spec directory in the repository are the authoritative description; the diagram is the fastest way to see the shape of the data flow.

Transport is Server-Sent Events, which is why the repository carries the server-sent-events and streaming-api topics. SSE is one-way by design: the server pushes, the client listens. That single decision explains most of the project's character. It works over plain HTTP, it reconnects, and it does not require a separate protocol upgrade the way WebSocket does. The repository also lists websocket among its topics, so the hub is not limited to SSE alone, but the push model and the battery-efficiency claim in the README both point at the one-way case.

The hub's job is authorization and fan-out. Subscribers present a JWT, and the repository's file layout shows how seriously that is treated: authorization.go, authorizationclaims_notdeprecated.go, authorizationdetails.go, claimrequirestopic_deprecated.go, jwtkeyfunc.go and bearererror.go are all top-level files, alongside tests for each. Topic matching uses URL patterns, and go.mod pulls in github.com/dunglas/go-urlpattern and github.com/yosida95/uritemplate/v3, which tells you the matching is pattern-based rather than simple string equality. The authorization model is therefore the part of the hub you will spend the most time configuring, and the part where a mistake is a security problem rather than a bug.

State lives in BoltDB. go.mod requires go.etcd.io/bbolt, and the repository has bolt.go with its own tests. That means the hub persists subscriptions and update history to a local file rather than to an external store by default, which keeps deployment simple and makes the persistence question a disk question. Observability is handled through Prometheus and OpenTelemetry, both present in go.mod, so metrics and tracing are available without bolting on a separate agent.

## Installing the hub with Docker and publishing a first update

The README points to the getting-started page and the hub install page for setup, and the repository contains a Dockerfile that builds on caddy:2-alpine. The image copies the mercure binary over /usr/bin/caddy and copies the repository's Caddyfile to /etc/caddy/Caddyfile. That is worth pausing on: the hub is a Caddy build. If you already run Caddy, the operational model will be familiar, and if you do not, you are adopting Caddy's configuration style along with Mercure.

The Dockerfile also defines a readiness check against the Caddy admin API on port 2019:

```dockerfile
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=5 \
	CMD ["wget", "-q", "--spider", "http://127.0.0.1:2019/mercure/health/ready"]
```

That endpoint is what your orchestrator should probe. Note the 60 second start period: the hub is not expected to be ready immediately, and a shorter startup probe will produce false failures.

The repository ships three example directories, and they are the honest starting point for a first real use rather than a synthetic hello-world. examples/publish/ shows the publisher side, examples/subscribe/ shows the subscriber side, and examples/chat/ shows both together. The README also links a hosted demo at demo.mercure.rocks. The safest first exercise is to read examples/publish and examples/subscribe side by side, run them against a local hub, and confirm you can see an update arrive on the subscriber connection after the publisher sends it. That exercises the JWT configuration, the topic IRI, and the transport in one pass, which is exactly the set of things that go wrong first.

For Go developers, the library path avoids the hub entirely. go.mod declares module github.com/dunglas/mercure and requires Go 1.27, so the package is importable directly. The README states the library can be used in any Go application to implement the Mercure protocol without a hub. The trade-off is real: you skip a network hop and a separate process, and you also take on the fan-out and connection management yourself. The repository's own example_test.go and e2e_test.go are the closest thing to worked examples of the library API.

## Where the hub is the wrong tool

The most common mismatch is expecting a message broker. Mercure pushes updates about resources to subscribers. It does not give you queues, delivery guarantees to offline consumers, or a topic model where a consumer group competes for messages. If your problem is work distribution between backend services, this is not that tool, and the README never claims otherwise.

The second mismatch is bidirectional traffic. SSE is one-way, and the README's framing is consistently about pushing updates to clients. If your application needs the client to send a stream of messages back over the same channel, WebSocket is the more direct fit, and adopting Mercure to get there means fighting the model. The repository lists websocket as a topic, so the hub has some WebSocket capability, but the protocol's described purpose is push, not duplex conversation.

A third limitation is operational, and it comes from the dependency list rather than from any warning in the README. State is in BoltDB by default. A single hub process with a local BoltDB file is a single point of failure and a single write target. The README advertises a managed high-scalability version separately, which is a signal that scaling the hub beyond one node is not the free-software default. If your availability target rules out one process with a local file, budget for that work explicitly; the repository does not present a turnkey clustered mode.

The last gap is documentation depth in this repository itself. The README is short and defers to mercure.rocks for getting started, full documentation and installation. The repository does not document rollback, upgrade paths between hub versions, or migration of the BoltDB file. Those answers live on the website, not here, and you should confirm them there before you pin a version in production.

## Alternative approaches and how they differ

The closest alternative in kind is running your own WebSocket server and pushing updates over it. The difference is not performance, it is the shape of the contract. A WebSocket server gives you a raw duplex socket and leaves message format, topic routing, authorization and reconnection semantics to you. Mercure specifies all four: topics are IRIs, authorization is JWT-based with topic claims, and the transport is SSE, which reconnects on its own. You trade flexibility for a defined contract. If you want to design your own wire format, the WebSocket route is more honest about what you are signing up for.

The second alternative is polling. It requires no new server component and no new protocol. It is also the thing Mercure exists to replace, and the README's battery-efficiency claim is aimed squarely at mobile clients polling on a timer. Polling is the right choice when updates are rare enough that the polling interval is not a cost, and the wrong choice when clients hold long-lived sessions and expect to see changes quickly.

The third alternative is the managed Mercure.rocks hub. Same protocol, same client code, different operational burden. The README describes it as managed and high-scalability, which addresses the clustering limitation above. The decision is the usual self-hosted versus managed trade: you keep control and avoid a recurring cost, and you take on upgrades, persistence, monitoring and availability. The repository's AGPL-3.0 licence is a factor here too, and it is worth reading the licence page the README links before you assume either path is equivalent for your distribution model.

## Licence, upgrade cost and maintenance signals

The hub is AGPL-3.0. The Dockerfile labels the image as AGPL-3.0-or-later. The README links a licence page at mercure.rocks/docs/hub/license and a COPYRIGHT file sits at the repository root. AGPL is a network copyleft licence, which means the obligations attach when users interact with the software over a network. Whether that reaches your application depends on how you combine it with your own code, and that is a question for your own counsel, not for this review. The practical point is that AGPL-3.0 is a materially different proposition from a permissive licence, and it should be a deliberate decision rather than a detail discovered late.

On maintenance, the repository is not archived, and the last push was on 2026-09-22. The recent release history shows a v1.0.0 on 2026-09-16, preceded by v1.0.0-beta.1 on 2026-09-14 and v1.0.0-alpha.3 on 2026-08-04. Reaching 1.0.0 is a meaningful signal for anyone who has been waiting for a stable tag, but note that the 1.0.0 line is weeks old. Treat it as new.

Upgrade cost is where the repository is quiet. The go.mod file carries retract directives for v0.14.6 and v0.14.7 with the comments "Overwritten tag" and "CI problem", which is a reminder that tags in this project have been withdrawn before. The file layout also shows a pattern of deprecated and not-deprecated file pairs: authorization_deprecated.go alongside authorization_notdeprecated.go, and the same for authorization claims, claim-requires-topic and handler-topic. That is a project that renames and supersedes APIs rather than freezing them, so pinning a version and reading the deprecation pairs before upgrading is the cheap insurance. The repository does not document a migration procedure between hub versions; that belongs on the documentation site.

## Conclusion

Adopt dunglas/mercure if you serve a web API that needs one-way push to browsers and you are willing to run and operate a hub yourself, or if you are writing Go and want the protocol without a separate server. Do not adopt it if you need bidirectional messaging as the primary model, or if AGPL-3.0 is incompatible with how you distribute your product. Before committing, verify three things: which hub version you pin, how you will persist subscriptions and the update history, and whether your authorisation setup matches the JWT claims model the repository implements.

## FAQ

### What is dunglas/mercure used for?

It is a protocol for pushing data updates to web browsers and other HTTP clients, plus a Go reference implementation of a Mercure hub, the server side. The README describes it as especially useful for publishing async and real-time updates of resources served through web APIs to reactive web and mobile apps.

### Does dunglas/mercure require a separate server?

Not necessarily. The README states that the repository includes a Go library that can be used in any Go application to implement the Mercure protocol directly, without a hub. Running the hub is the other option, and the repository also provides an official Docker image for it.

### What transport does the Mercure hub use for real-time updates?

The repository lists server-sent-events and streaming-api among its topics, and the README frames the protocol around pushing updates to browsers and HTTP clients. The repository also lists websocket as a topic, so the hub is not limited to SSE alone.

### How is the Mercure hub deployed with Docker?

The Dockerfile builds on caddy:2-alpine, copies the mercure binary to /usr/bin/caddy and the repository Caddyfile to /etc/caddy/Caddyfile. It defines a readiness healthcheck against the Caddy admin API at http://127.0.0.1:2019/mercure/health/ready with a 60 second start period.

### What licence does dunglas/mercure use?

The hub is free software under AGPL-3.0, and the Dockerfile labels the image as AGPL-3.0-or-later. The README links a licence page at mercure.rocks/docs/hub/license and a COPYRIGHT file is present at the repository root.

## Sources

- [dunglas/mercure on GitHub](https://github.com/dunglas/mercure)
- [License: AGPL-3.0](https://github.com/dunglas/mercure/blob/main/LICENSE)
- [Project website](https://mercure.rocks)
- [README](https://github.com/dunglas/mercure/blob/main/README.md)
- [Releases](https://github.com/dunglas/mercure/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/dunglas-mercure
