Upgrade guide
1.0 (from 0.x)
The 1.0 release aligns the hub with the standards-based specification: two matcher types and an OAuth 2.0 authorization model. It is a breaking change for subscribers, publishers, and token issuers.
If you only run the hub and don't author clients or mint tokens, the upgrade is a config change. If you do, plan a synchronized cutover of the hub and the clients that talk to it. A hub built with the deprecated_topic and deprecated_claim tags can run protocol_version_compatibility 8 to keep accepting 0.x clients during the transition (see Compatibility mode).
What changed at a glance
| Area | 0.x | 1.0 |
|---|---|---|
| Matcher types | URI Template, string, plus exploratory types | exact and urlpattern only |
| Subscribe query parameter | topic=<pattern> (URI Template or string) | match=<exact> or match_urlpattern=<pattern> (case-sensitive) |
| Templating language | URI Templates (RFC 6570) | URL Patterns (WHATWG) |
| Token | bespoke mercure JWT claim | OAuth 2.0 access token: typ: at+jwt, iss, aud, authorization_details |
| Authorization | mercure.publish / mercure.subscribe string arrays | authorization_details entries with the Mercure type URI (see below) |
| Token in query / cookie | authorization param, mercureAuthorization cookie | __Secure-mercure_access_token cookie; no query parameter (RFC 9700) |
| Auth errors | 401 / silent drop | RFC 6750: 401 invalid_token, 403 insufficient_scope, 400 invalid_request |
| Subscription event topic | /.well-known/mercure/subscriptions/{topic}/{subscriber} | /.well-known/mercure/subscriptions/{match_type}/{match}/{subscriber} |
Migrate your subscribers
The query parameter changes from topic= to match= (exact) or match_urlpattern= (templated):
// Before (0.x) url.searchParams.append("topic", "https://example.com/books/1"); url.searchParams.append("topic", "https://example.com/books/{id}"); // After (1.0) url.searchParams.append("match", "https://example.com/books/1"); url.searchParams.append("match_urlpattern", "https://example.com/books/:id");
The exact-match parameter is
match(explicit spelling:match_exact); the templated one ismatch_urlpattern, using URL Pattern syntax (:id, not{id}).Parameter names are case-sensitive. Any other name under the
matchprefix is rejected with400, so typos fail loudly.The
URI Templatematcher type is gone; it survives only on a hub built withdeprecated_topicrunningprotocol_version_compatibility 8. Rewrite templated topics as URL Patterns and string topics as exact topics.
Migrate your tokens
The bespoke mercure claim is replaced by a standard OAuth 2.0 JWT access token: a typ: at+jwt header, an iss claim matching one of the hub's trusted issuers, an aud claim holding the hub's resource identifier, a required exp, and an authorization_details array. RFC 9068 also requires issuers to populate sub, client_id, iat, and jti.
// Before (0.x) { "mercure": { "publish": ["*"], "subscribe": [ "https://example.com/users/42", "https://example.com/books/{id}", ], }, }
// After (1.0) — header { "alg": "...", "typ": "at+jwt" } { "iss": "https://example.com", "aud": "https://hub.example.com/.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": "https://example.com/users/42" }, { "match": "https://example.com/books/:id", "match_type": "urlpattern", }, ], }, ], }
Rules:
Each entry is
{ "type": "https://mercure.rocks/authorization-detail", "actions": [...], "topics": [...] }.actionsis a non-empty subset of["publish", "subscribe"];topicsis a non-empty array of{ "match", "match_type"? }objects (bare strings are rejected).match_typeis case-sensitive and defaults toexact. The reserved{ "match": "*" }matches every topic.A
subscribeentry may carry apayload; the old top-levelmercure.payloadis gone. See subscriber payloads.One invalid Mercure detail rejects the whole token with
401 invalid_token.
Migrate token presentation
The cookie default name changes from
mercureAuthorizationto__Secure-mercure_access_token(override withcookie_name; use a prefix-less name for plain-HTTP development).The
authorizationquery parameter is gone and has no replacement: RFC 9700 forbids access tokens in URLs. Browsers that can't set headers use the cookie; everything else (includingfetch()with a readable stream) uses theAuthorizationheader.The
Authorization: Bearerheader is unchanged and takes precedence over the cookie.
Migrate to RFC 6750 errors
Authorization failures now follow RFC 6750:
No token where one is required ->
401with a bareWWW-Authenticate: Bearerchallenge and aresource_metadataparameter.Invalid token ->
401error="invalid_token".Valid token without a grant for the action on the topic ->
403error="insufficient_scope"(previously401or a silent drop).Malformed request ->
400error="invalid_request".
Migrate the subscription API and events
| Before | After |
|---|---|
/.well-known/mercure/subscriptions/<topic>/<subscriber> | /.well-known/mercure/subscriptions/<match_type>/<match>/<subscriber> |
"topic": "https://..." in the JSON-LD | "match": "https://..." and "match_type": "urlpattern" |
<match_type>, <match>, and <subscriber> must be percent-encoded. The mercure.subscriber claim is gone: the hub assigns the subscriber identifier. See Active subscriptions.
Find-and-replace checklist
?topic=/&topic=in subscriber URLs ->match=(ormatch_urlpattern=if templated)URI Template syntax in subscribe URLs (
{id}) -> URL Pattern syntax (:id)"mercure": { "publish": [...] }in issuer code ->authorization_detailswithactions: ["publish"]"mercure": { "subscribe": [...] }->authorization_detailswithactions: ["subscribe"]mercureAuthorizationcookie ->mercure_access_token;authorization=query param ->Authorizationheader or cookie (no query parameter)Hardcoded
subscriptions/{topic}/{subscriber}paths -> add the{match_type}segmentLast-Event-IDread from a response ->Mercure-Last-Event-ID(the request header keeps its name; see Reconnection and history)JSON-LD subscription documents (
application/ld+json,@context) -> plain JSON served asapplication/jsontypevalues lowercased:Subscription->subscription,Subscriptions->subscriptionsSubscription events now carry the SSE
event: mercurefield (route them withaddEventListener("mercure", ...)); publishing an update whosetypeismercureis rejected with a400 Bad Request
Hub configuration changes
New in 1.0 (nothing to migrate)
resource_identifier, public_urls, and RFC 9728 discovery have no 0.x equivalent. There's no prior config to translate here, only something new to configure if you want it.
The hub derives its public URL, the OAuth 2.0 resource identifier (token
aud) and the RFC 9728 metadata from each request, so a hub reachable through several public URLs works with no domain configuration. Setresource_identifieronly to pin one canonical audience shared across every domain. On a catch-all site block (:443, no host matcher), addpublic_urls <url...>so a request whose origin is not listed is rejected with421 Misdirected Requestinstead of choosing the derived identity.
Changed configuration
Declare your token issuer with an
issuer <id> { ... }block binding theissvalue your tokens carry to itspublisher/subscriberverifier (jwtorjwks_uri); it's required when JWT auth is enabled in modern mode. Addauthorization_serverinside the block to advertise it (see Discovery). Repeat the block to trust several issuers with distinct keys.The pre-1.0 top-level directives
publisher_jwt,subscriber_jwt,publisher_jwks_urlandsubscriber_jwks_urlstill parse but map to a single implicit issuer usable only in compatibility mode. Setting one withoutprotocol_version_compatibilityis now a configuration error, because that mode also drops the requiredexp, the audience check, theat+jwtcheck and the issuer check, and re-accepts the token in the URL query string. Migrate them into anissuerblock for modern mode, or addprotocol_version_compatibility 8to accept those trade-offs deliberately.The official Caddyfile no longer redacts query parameters from logs or serves
/healthz; both only mattered for 0.x clients. Restore them if you run compatibility mode.transport_url(deprecated since 0.17) is removed; usetransport <name> { ... }.An unrecognized directive inside the
mercureblock is now a configuration error instead of being ignored. A typo previously disabled whatever it was meant to configure, silently, so check yourMERCURE_EXTRA_DIRECTIVESif the hub refuses to start after the upgrade.The
uianddemodirectives were renamed.uiis nowdebugger: the prod-safe debugger UI, which also moved from/.well-known/mercure/ui/to/.well-known/mercure/debug/.demois nowplayground(the insecure playground: it additionally mints an all-access token prefilled in the UI, and its echo endpoints moved out of the reserved hub namespace, from/.well-known/mercure/ui/demo/to the root/playground/path, so the resources they expose are valid, subscribable topics). Because unknown directives now error, rename them in your Caddyfile.playgroundnow turns on the permissive dev settings it needs on its own:anonymous,subscriptions, wildcardcors_origins/publish_origins, and a prefix-lesscookie_name, unless you set them explicitly, and redirects the site root/to the debugger UI. This is INSECURE; never enableplaygroundin production.The bundled
dev.Caddyfilewas removed. It was only the defaultCaddyfileplusplayground, so run a development hub with the default config andMERCURE_EXTRA_DIRECTIVES=playground(ordebuggerfor the prod-safe UI without a token). The Docker image and the Helm chart'sdev: truevalue do this for you.
Legacy non-Caddy server removed
The standalone non-Caddy binary is gone. It's been deprecated since Mercure 0.11, when the Caddy module became the primary hub, so this shouldn't affect anyone still on a supported setup. If you're still running it: switch to the Caddy-based binary or Docker image everyone else already uses (see Installation). There's no flag-for-flag migration to give, because this isn't a config change, it's a different binary. Compatibility mode restores 0.x protocol behaviors on the Caddy-based hub only; it doesn't bring the removed binary back.
Compatibility mode
0.x behaviors are gated behind two build tags, honored only with protocol_version_compatibility 8:
deprecated_topic: URI Template selectors intopic=, bare-string JWT matcher claims, the/subscriptions/{topic}routes. Canonical and alternate topics (repeatedtopic=publish fields) are a modern-mode feature, not gated by this tag; see Alternate topics.deprecated_claim: the legacymercureclaim (string and object forms), thehttps://mercure.rocks/namespaced claim,mercure.payload, theauthorizationquery parameter, themercureAuthorizationcookie, and tokens withouttyp: at+jwt,aud,expor a matchingiss.
Enabling it therefore weakens access-token validation, which is why the hub never turns it on by itself.
Official binaries and Docker images ship with both tags, so you can run protocol_version_compatibility 8 during the migration. A hub built without a tag rejects the corresponding 0.x behavior outright. Custom builds must pass the tags to go build.
Restore the removed Caddyfile directives
The official Caddyfile dropped two directives that only serve 0.x clients. Add them back to your Caddyfile when running compatibility mode.
0.x clients pass the token in the authorization query parameter, so keep it out of logs by restoring the log filter inside the site block:
log { format filter { fields { request>uri query { replace authorization REDACTED } } } }
The deprecated /healthz endpoint (superseded by the /mercure/health/ready and /mercure/health/live admin API endpoints):
log_skip /healthz respond /healthz 200
Go API changes
canReceive/canDispatchare replaced by the internal authorization-detail grant logic.WithUIis renamedWithDebugger, andWithDemois renamedWithPlayground. The playground can prefill an access token via the newWithPlaygroundTokenFunc(INSECURE, EXPERIMENTAL).NewHubno longer requires a resource identifier: it derives the identity from each request, resolving the origin fromNewRequestOriginContextwhen an embedding server sets it, else from the request's scheme andHost.WithResourceIdentifierstill pins one static value; a value ending in/.well-known/mercurealso sets the URL Pattern base.WithPublicURLsrestricts the hub to an allowlist of public URLs (scheme and host), returning421for an unlisted origin.
Historical changes (0.x)
The entries below describe earlier upgrades. They are kept for users migrating across multiple major versions.
Mercure 0.21 upgrade notes
When Mercure is compiled manually or used as a Go library, deprecated features are no longer included by default.
To re-enable deprecated transports, pass the deprecated_transport build tag when compiling Mercure:
# Mercure 0.21 Upgrade Notes go build -tags deprecated_transport
Official binaries and Docker images still include deprecated features.
Mercure 0.17 upgrade notes
The MERCURE_TRANSPORT_URL environment variable and the transport_url directive were deprecated in favor of the transport directive.
Before:
# Mercure 0.17 Upgrade Notes transport_url bolt://mercure.db?cleanup_frequency=0.2
After:
# Mercure 0.17 Upgrade Notes transport bolt { path mercure.db cleanup_frequency 0.2 }
To configure the transport via an environment variable, append the directive to MERCURE_EXTRA_DIRECTIVES. Avoid putting credentials there; use {env.MY_VAR} placeholders in a custom Caddyfile instead.
Mercure 0.16.2 upgrade notes
Caddyfile.dev was renamed to dev.Caddyfile to match Caddy best practices.
Mercure 0.14.4 upgrade notes
This release moved to Caddy 2.6, which removed single-hyphen long-form flags. Use --config instead of -config.
Mercure 0.14.3 upgrade notes
The Prometheus metric mercure_subscribers was renamed mercure_subscribers_connected for better interoperability with Datadog and others.
Mercure 0.14.1 upgrade notes
The default development key changed from !ChangeMe! to !ChangeThisMercureHubJWTSecretKey! to satisfy the spec's 256-bit minimum.
Mercure 0.14 upgrade notes
The Last-Event-ID query parameter was renamed last_event_id. Update your clients.
Publishing public updates in topics not listed in mercure.publish was removed; use ["*"] to keep the old behavior.
A protocol_version_compatibility 7 directive was added to ease the transition. It has since been removed in 1.0.
Mercure 0.13 upgrade notes
The DEBUG environment variable was removed. Set GLOBAL_OPTIONS=debug instead.
Mercure 0.11 upgrade notes
The hub became a Caddy module. Standalone binaries are now custom Caddy builds. The legacy server stayed available with a legacy build prefix until 1.0.
Before switching, migrate your configuration.
Mercure 0.10 upgrade notes
The protocol changed substantially. Highlights:
Targets are gone, replaced by topic selectors. Mark updates
privateand check themercure.publish/mercure.subscribeclaims.Subscription JSON-LD:
"@type": "https://mercure.rocks/Subscription"->"type": "Subscription".dispatch_subscriptions->subscriptions.subscriptions_include_ipremoved; usemercure.payload.IDs are now URNs (
urn:uuid:...).*as a topic became reserved.
Mercure 0.8 upgrade notes
Hub URL changed from
/hubto/.well-known/mercure.HISTORY_CLEANUP_FREQUENCY,HISTORY_SIZE,DB_PATHcollapsed intoTRANSPORT_URL.ACME_HOSTS,CORS_ALLOWED_ORIGINS,PUBLISH_ALLOWED_ORIGINSswitched to space-separated values.The Go library's public API was rewritten.