Mercure 1.0 alpha is available. Check out the new docs
Sponsored by Les-Tilleuls.coop
DocumentationSpecificationCloudDemos
Contribute!

Quickstart

This guide gets you from zero to a real-time update in your browser in five minutes. We'll run a hub locally with Docker, subscribe from a one-liner HTML page, and publish from curl.

If you already have a hub running, jump to Subscribe or Publish.

Run the Mercure hub locally with Docker

# Run the Mercure Hub Locally with Docker
docker run -p 80:80 -p 443:443 -p 443:443/udp \
  -e MERCURE_EXTRA_DIRECTIVES=playground \
  dunglas/mercure

The hub is now serving on https://localhost.

What that command does:

  • -p 443:443/udp: Caddy serves HTTP/3 over QUIC on this port too. Without it, the container still starts and HTTP/1.1 and HTTP/2 both work, but clients silently fall back past HTTP/3.

  • MERCURE_EXTRA_DIRECTIVES=playground: turns on the insecure playground, which enables anonymous subscriptions, permissive CORS, and the in-browser debugger at https://localhost/.well-known/mercure/debug/ with a prefilled all-access token signed with a well-known default secret (no MERCURE_*_JWT_KEY needed). Drop it for production; the installation guide covers the default config and proper key management.

Because SERVER_NAME defaults to localhost, Caddy serves real HTTPS on it: an internal, self-signed certificate, since localhost can't get a publicly trusted one. Open https://localhost/.well-known/mercure/debug/ in your browser and accept the certificate warning once; that's expected for local dev, and it doubles as a check that the hub is up. This also means the hub's default trusted issuer (https://localhost) and its derived resource identifier (https://localhost/.well-known/mercure) already match the example token below, so nothing needs pinning.

Pro tip. Don't want to manage a hub? Mercure Cloud has a free tier sized for prototyping. Same protocol, no infrastructure to run.

Subscribe to a Mercure topic from the browser

Save this as index.html and open it in your browser:

<!-- index.html -->
<!doctype html>
<title>Mercure quickstart</title>
<ul id="log"></ul>
<script>
  const url = new URL("https://localhost/.well-known/mercure");
  url.searchParams.append("match", "https://example.com/books/1");

  const es = new EventSource(url);
  es.onmessage = (event) => {
    const li = document.createElement("li");
    li.textContent = event.data;
    document.getElementById("log").prepend(li);
  };
</script>

The match query parameter does an exact-match subscription on the topic https://example.com/books/1. To subscribe to a family of URLs at once, use match_urlpattern:

// Subscribe to a Mercure Topic from the Browser
url.searchParams.append("match_urlpattern", "https://example.com/books/:id");

URL patterns follow the WHATWG URL Pattern syntax. They replace URI templates as the recommended templating language for URL topics. Topics and matchers covers the full set.

Publish a Mercure update with curl

In another terminal:

# Publish a Mercure Update with curl
curl --insecure -X POST https://localhost/.well-known/mercure \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6ImF0K2p3dCJ9.eyJhdWQiOiJodHRwczovL2xvY2FsaG9zdC8ud2VsbC1rbm93bi9tZXJjdXJlIiwiYXV0aG9yaXphdGlvbl9kZXRhaWxzIjpbeyJhY3Rpb25zIjpbInB1Ymxpc2giXSwidG9waWNzIjpbeyJtYXRjaCI6IioifV0sInR5cGUiOiJodHRwczovL21lcmN1cmUucm9ja3MvYXV0aG9yaXphdGlvbi1kZXRhaWwifSx7ImFjdGlvbnMiOlsic3Vic2NyaWJlIl0sInRvcGljcyI6W3sibWF0Y2giOiIqIn1dLCJ0eXBlIjoiaHR0cHM6Ly9tZXJjdXJlLnJvY2tzL2F1dGhvcml6YXRpb24tZGV0YWlsIn1dLCJleHAiOjQxMDI0NDQ4MDAsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0In0.VO0-PRjJ2MGOrMk2HxlrBv217pB7hyLxLIQUGgSfyXs' \
  -d 'topic=https://example.com/books/1' \
  -d 'data={"status": "checked out"}'

--insecure skips certificate verification, needed here only because the dev hub's certificate is self-signed. Drop it once you point curl at a hub with a real certificate.

Reload the browser tab. The new message appears at the top of the list.

The bearer token is an OAuth 2.0 access token signed with the dev key above (header { "alg": "HS256", "typ": "at+jwt" }), carrying:

// Publish a Mercure Update with curl
{
  "iss": "https://localhost",
  "aud": "https://localhost/.well-known/mercure",
  "exp": 4102444800,
  "authorization_details": [
    {
      "type": "https://mercure.rocks/authorization-detail",
      "actions": ["publish"],
      "topics": [{ "match": "*" }],
    },
    {
      "type": "https://mercure.rocks/authorization-detail",
      "actions": ["subscribe"],
      "topics": [{ "match": "*" }],
    },
  ],
}

The iss matches the hub's default trusted issuer, the aud matches its derived resource identifier, the typ header is at+jwt, and the publish grant covers every topic. Generate your own with caddy mercure-token --dev — it mints an equivalent token (the same issuer, audience, and grants, plus the sub/client_id/iat/jti claims this hub doesn't require but RFC 9068 does). Details in Authorization.

Closing the Mercure EventSource connection

EventSource keeps the TCP connection open as long as the page lives. Single-page apps in particular should call es.close() when the component that opened the stream unmounts:

// Closing the Mercure EventSource Connection
useEffect(() => {
  const es = new EventSource(url);
  es.onmessage = (e) => /* ... */;
  return () => es.close();
}, [url]);

Otherwise, the browser keeps the connection alive on cached pages and the hub keeps the slot allocated.

Mercure quickstart: publish/subscribe flow recap

# Mercure Quickstart: Publish/Subscribe Flow Recap
            POST /.well-known/mercure       GET /.well-known/mercure?match=...
publisher  ----------------------->  hub  <-----------------------------  subscriber
                                  (HTTP/2,                              (Server-Sent
                                   one TCP                               Events,
                                   per client)                           one TCP)

The hub is the only piece you need to deploy. Publishers can be anywhere: your existing API server, a worker, a serverless function, a GitHub webhook. Subscribers use plain EventSource, so anything that talks HTTP can subscribe.

Mercure quickstart next steps