Community
Mercure 1.0 is final. When the protocol was drafted in 2018, the web standards covering its needs did not exist, so Mercure grew its own machinery. They exist now, so 1.0 drops the bespoke parts and the hacks, and reuses them. That is the through-line of the whole release. Topic matching moves to URL Patterns — itself a new web standard, and the syntax browsers already ship — in place of URI Templates, and a subscription now states which kind of match it wants: match= for an exact topic, match_urlpattern= for a pattern. No more guessing whether {id} was a template or a literal. Authorization is plain OAuth 2.0, and the hub is a generic resource server an off-the-shelf library can enforce.
Defaults are stricter throughout. The built-in UI is production-safe, and everything insecure about it moved behind a separate opt-in. Access tokens are never accepted in a URL. A mistyped directive stops the hub instead of silently disabling whatever it was meant to configure. A valid token without a grant gets a 403 rather than a silent drop. Compatibility with 0.x weakens token validation, so the hub never turns it on by itself.
They are breaking changes for subscribers, publishers, and token issuers. If you only run the hub, the upgrade is a config change. Otherwise, plan a synchronized cutover — or turn on compatibility mode and keep serving 0.x clients while you migrate. Everything below is in the upgrade guide.
✨ New Features
- URL Pattern matchers. Templated topics use the pattern syntax browsers ship natively (
:id, wildcards, groups) instead of URI Templates, so the same pattern object can be reused client-side. A subscription picks its matcher explicitly —match=for exact,match_urlpattern=for a pattern — andmatch_typedoes the same inside a token, defaulting toexact. QUERYsubscriptions (RFC 10008). Matcher parameters travel in anapplication/x-www-form-urlencodedrequest body instead of the query string, so a subscriber watching hundreds of topics no longer hits the 4–8 KB URL limits browsers, proxies, and CDNs enforce. Safe and idempotent likeGET.EventSourcecan't issue it, so use afetch-based client. The hub advertises support withAccept-Queryon every subscription answer, including refusals.- A production-safe debugger. The old
uibecomesdebugger, which ships safe defaults and can be left on in production. The insecure parts moved toplayground, which mints an all-access token and turns onanonymous,subscriptions, wildcard origins and a prefix-less cookie name on its own. Never enableplaygroundin production. mercure-tokencommand, minting self-issued access tokens so a hub is usable before you stand up an authorization server.- RFC 9728 discovery. The hub advertises protected resource metadata, and
authorization_serverinside anissuerblock advertises where to get a token. - Request-derived identity. The hub computes its public URL and OAuth 2.0 resource identifier from each request, so one hub reachable through several domains needs no configuration.
resource_identifierpins a single canonical audience instead; on a catch-all site block,public_urlsrejects an unlisted origin with421 Misdirected Request. max_request_body_sizecaps publish andQUERYsubscribe bodies (default1MiB,413above it);0delegates to a reverse proxy.Incremental: ?1response header (RFC 10036), the standardized counterpart of theX-Accel-Buffering: nothe hub already sent.
⚠️ Breaking Changes
Matchers. topic= becomes match= (exact) or match_urlpattern= (WHATWG URL Pattern, :id rather than {id}). URI Templates are gone. Parameter names are case-sensitive and any other name under the match prefix is rejected with 400, so typos fail loudly instead of silently widening a subscription.
Tokens. The bespoke mercure JWT claim is replaced by an OAuth 2.0 access token: typ: at+jwt, an iss matching a trusted issuer, an aud holding the hub's resource identifier, a required exp, and an authorization_details array of { "type": "https://mercure.rocks/authorization-detail", "actions": [...], "topics": [...] } entries. topics holds { "match", "match_type"? } objects — bare strings are rejected, match_type is case-sensitive and defaults to exact, and { "match": "*" } still matches everything. A subscribe entry carries its own payload; the top-level mercure.payload is gone. The claim must be unambiguous JSON: a duplicate member or invalid UTF-8 rejects the token rather than letting the hub pick a reading the authorization server may not have validated. One invalid detail rejects the whole token.
Token presentation. The cookie is __Secure-mercure_access_token (was mercureAuthorization); override with cookie_name. The authorization query parameter is gone with no replacement — RFC 9700 forbids access tokens in URLs. Authorization: Bearer is unchanged and still wins over the cookie.
Authorization errors follow RFC 6750: no token where one is required gets 401 with a WWW-Authenticate: Bearer challenge carrying resource_metadata, an invalid token 401 invalid_token, a valid token without a grant 403 insufficient_scope (previously 401 or a silent drop), and a malformed request 400 invalid_request.
Subscription API and events.
- The path gains a match-type segment:
/.well-known/mercure/subscriptions/<match_type>/<match>/<subscriber>. "topic"in the document becomes"match"plus"match_type".- The
mercure.subscriberclaim is gone; the hub assigns the subscriber identifier. - Documents are plain JSON served as
application/json, not JSON-LD — no@context.typevalues are lowercased:Subscription→subscription,Subscriptions→subscriptions. - Documents serialize compactly, and
<,>,&are no longer escaped as<,>,&. Both forms are the same JSON, so only a client string-matching raw bytes needs updating. A subscription event is now a singledata:line. - Subscription events carry the SSE
event: mercurefield — route them withaddEventListener("mercure", ...). Publishing an update whosetypeismercureis rejected with400.
The Last-Event-ID response header is now Mercure-Last-Event-ID. The request header keeps its name.
Hub configuration.
- Declare your token issuer with an
issuer <id> { ... }block binding theissyour tokens carry to itspublisher/subscriberverifier (jwtorjwks_uri). Required when JWT auth is on in modern mode; repeat it to trust several issuers with distinct keys. publisher_jwt,subscriber_jwt,publisher_jwks_urlandsubscriber_jwks_urlstill parse but map to one implicit issuer usable only in compatibility mode. Setting one withoutprotocol_version_compatibilityis now a configuration error.- An unrecognized directive inside the
mercureblock is a configuration error instead of being ignored — a typo used to silently disable whatever it configured. Check yourMERCURE_EXTRA_DIRECTIVESif the hub refuses to start. ui→debugger, and its UI moved from/.well-known/mercure/ui/to/.well-known/mercure/debug/.demo→playground, whose echo endpoints moved out of the reserved hub namespace to/playground/so they are valid subscribable topics.use_forwarded_headersis removed — the hub derives its public URL from the request.topic_selector_cache→topic_matcher_cache.transport_url(deprecated since 0.17) is removed; usetransport <name> { ... }.- The bundled
dev.Caddyfileis removed: it was the default config plusplayground. RunMERCURE_EXTRA_DIRECTIVES=playgroundinstead — the Docker image and the chart'sdev: truedo it for you. - The official Caddyfile no longer redacts query parameters from logs or serves
/healthz; both only mattered for 0.x clients. The guide shows how to restore them under compatibility mode.
The legacy non-Caddy server is removed. Deprecated since 0.11, when the Caddy module became the primary hub. Its configuration goes with it; there is no flag-for-flag migration because it is a different binary. Compatibility mode restores 0.x protocol behaviors on the Caddy hub only.
Go API. WithUI → WithDebugger, WithDemo → WithPlayground (with a new WithPlaygroundTokenFunc, INSECURE and EXPERIMENTAL). canReceive / canDispatch are replaced by the internal authorization-detail grant logic. NewHub no longer requires a resource identifier — it derives identity per request, resolving the origin from NewRequestOriginContext when an embedding server sets one. WithResourceIdentifier still pins a static value; WithPublicURLs restricts the hub to an allowlist.
🔁 Compatibility mode
protocol_version_compatibility 8 keeps 0.x clients working during the migration, gated behind two build tags: deprecated_topic (URI Template selectors, bare-string matcher claims, the /subscriptions/{topic} routes) and deprecated_claim (the legacy mercure claim, mercure.payload, the authorization query parameter, the mercureAuthorization cookie, and tokens without typ: at+jwt, aud, exp or a matching iss).
It weakens access-token validation, which is why the hub never turns it on by itself. Official binaries and Docker images ship both tags; custom builds must pass them to go build.
🐛 Bug Fixes
- Pin the JWT algorithm allowlist in compatibility mode, closing an algorithm-confusion path on legacy tokens. by @dunglas in #1298
- Expose
Mercure-Last-Event-Idto cross-origin subscribers. Without it a fetch-based cross-origin subscriber cannot read the resumption cursor, so it cannot detect data loss when reconnecting. by @dunglas in #1385 - Announce a subscription only once it exists, closing a window where the subscription API advertised a subscriber that could not yet receive updates. by @dunglas
- Don't close a connection whose write deadline is near, which dropped long-lived subscribers early. by @dunglas
- Don't sort the update's own topic slice while dispatching — a data race on a caller-owned slice. by @dunglas
- Bolt: report
earliestwhen the requestedLast-Event-IDis missing, instead of silently starting from the newest update. by @dunglas in #1321 - Bolt:
cleanup_frequencyran the cleanup with the inverse probability, so a low value cleaned aggressively and a high one barely at all. by @dunglas - Chart: guard the
httpRoutetemplates against null or absent values. by @dunglas in #1268
⚡ Performance and Internal Improvements
- 19.5% smaller subscription SSE frames. Subscription documents serialize with
encoding/json/v2without indentation: every JSON newline used to cost a furtherdata:prefix on the wire. by @dunglas in #1378 - Go 1.27, with the standard library
uuidpackage replacinggithub.com/gofrs/uuid. by @dunglas in #1378
📖 Documentation
- The spec got a full IETF-style editorial and security pass. by @dunglas in #1262
- The documentation was restructured into getting started, concepts, deployment, production, and use cases, with new pages on authorization, topics and matchers, subscribing, publishing, discovery, active subscriptions, reconnection and history and update payloads.
- New use cases: LLM token streaming and notifications.
- How to issue Mercure tokens from an OAuth 2.0 authorization server, including a Keycloak RAR provider. by @dunglas
- Complete RFC 9068 token example. by @jems in #1299
- README fixes. by @TomKeur in #1369
🎤 Keynote tomorrow at API Platform Con
The opening keynote at API Platform Con is all about Mercure 1.0. Join us in Lille, France, or online on September 17 and 18, 2026. Grab your ticket!
Enterprise
Mercure Cloud and Mercure Enterprise 1.0 are generally available. The on-prem hub ships as image v1.0.0.
💖 New Contributors
- @jems made their first contribution in #1299
- @pseidemann made their first contribution in #1366
- @TomKeur made their first contribution in #1369
Full Changelog: v0.24.2...v1.0.0