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

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

Area0.x1.0
Matcher typesURI Template, string, plus exploratory typesexact and urlpattern only
Subscribe query parametertopic=<pattern> (URI Template or string)match=<exact> or match_urlpattern=<pattern> (case-sensitive)
Templating languageURI Templates (RFC 6570)URL Patterns (WHATWG)
Tokenbespoke mercure JWT claimOAuth 2.0 access token: typ: at+jwt, iss, aud, authorization_details
Authorizationmercure.publish / mercure.subscribe string arraysauthorization_details entries with the Mercure type URI (see below)
Token in query / cookieauthorization param, mercureAuthorization cookie__Secure-mercure_access_token cookie; no query parameter (RFC 9700)
Auth errors401 / silent dropRFC 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 is match_urlpattern, using URL Pattern syntax (:id, not {id}).

  • Parameter names are case-sensitive. Any other name under the match prefix is rejected with 400, so typos fail loudly.

  • The URI Template matcher type is gone; it survives only on a hub built with deprecated_topic running protocol_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": [...] }. actions is a non-empty subset of ["publish", "subscribe"]; topics is a non-empty array of { "match", "match_type"? } objects (bare strings are rejected).

  • match_type is case-sensitive and defaults to exact. The reserved { "match": "*" } matches every topic.

  • A subscribe entry may carry a payload; the old top-level mercure.payload is 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 mercureAuthorization to __Secure-mercure_access_token (override with cookie_name; use a prefix-less name for plain-HTTP development).

  • The authorization query 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 (including fetch() with a readable stream) uses the Authorization header.

  • The Authorization: Bearer header 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 -> 401 with a bare WWW-Authenticate: Bearer challenge and a resource_metadata parameter.

  • Invalid token -> 401 error="invalid_token".

  • Valid token without a grant for the action on the topic -> 403 error="insufficient_scope" (previously 401 or a silent drop).

  • Malformed request -> 400 error="invalid_request".

Migrate the subscription API and events

BeforeAfter
/.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= (or match_urlpattern= if templated)

  • URI Template syntax in subscribe URLs ({id}) -> URL Pattern syntax (:id)

  • "mercure": { "publish": [...] } in issuer code -> authorization_details with actions: ["publish"]

  • "mercure": { "subscribe": [...] } -> authorization_details with actions: ["subscribe"]

  • mercureAuthorization cookie -> mercure_access_token; authorization= query param -> Authorization header or cookie (no query parameter)

  • Hardcoded subscriptions/{topic}/{subscriber} paths -> add the {match_type} segment

  • Last-Event-ID read 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 as application/json

  • type values lowercased: Subscription -> subscription, Subscriptions -> subscriptions

  • Subscription events now carry the SSE event: mercure field (route them with addEventListener("mercure", ...)); publishing an update whose type is mercure is rejected with a 400 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. Set resource_identifier only to pin one canonical audience shared across every domain. On a catch-all site block (:443, no host matcher), add public_urls <url...> so a request whose origin is not listed is rejected with 421 Misdirected Request instead of choosing the derived identity.

Changed configuration

  • Declare your token issuer with an issuer <id> { ... } block binding the iss value your tokens carry to its publisher/subscriber verifier (jwt or jwks_uri); it's required when JWT auth is enabled in modern mode. Add authorization_server inside 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_url and subscriber_jwks_url still parse but map to a single implicit issuer usable only in compatibility mode. Setting one without protocol_version_compatibility is now a configuration error, because that mode also drops the required exp, the audience check, the at+jwt check and the issuer check, and re-accepts the token in the URL query string. Migrate them into an issuer block for modern mode, or add protocol_version_compatibility 8 to 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; use transport <name> { ... }.

  • An unrecognized directive inside the mercure block is now a configuration error instead of being ignored. A typo previously disabled whatever it was meant to configure, silently, so check your MERCURE_EXTRA_DIRECTIVES if the hub refuses to start after the upgrade.

  • The ui and demo directives were renamed. ui is now debugger: the prod-safe debugger UI, which also moved from /.well-known/mercure/ui/ to /.well-known/mercure/debug/. demo is now playground (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.

  • playground now turns on the permissive dev settings it needs on its own: anonymous, subscriptions, wildcard cors_origins/publish_origins, and a prefix-less cookie_name, unless you set them explicitly, and redirects the site root / to the debugger UI. This is INSECURE; never enable playground in production.

  • The bundled dev.Caddyfile was removed. It was only the default Caddyfile plus playground, so run a development hub with the default config and MERCURE_EXTRA_DIRECTIVES=playground (or debugger for the prod-safe UI without a token). The Docker image and the Helm chart's dev: true value 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 in topic=, bare-string JWT matcher claims, the /subscriptions/{topic} routes. Canonical and alternate topics (repeated topic= publish fields) are a modern-mode feature, not gated by this tag; see Alternate topics.

  • deprecated_claim: the legacy mercure claim (string and object forms), the https://mercure.rocks/ namespaced claim, mercure.payload, the authorization query parameter, the mercureAuthorization cookie, and tokens without typ: at+jwt, aud, exp or a matching iss.

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 / canDispatch are replaced by the internal authorization-detail grant logic.

  • WithUI is renamed WithDebugger, and WithDemo is renamed WithPlayground. The playground can prefill an access token via the new WithPlaygroundTokenFunc (INSECURE, EXPERIMENTAL).

  • NewHub no longer requires a resource identifier: it derives the identity from each request, resolving the origin from NewRequestOriginContext when an embedding server sets it, else from the request's scheme and Host. WithResourceIdentifier still pins one static value; a value ending in /.well-known/mercure also sets the URL Pattern base. WithPublicURLs restricts the hub to an allowlist of public URLs (scheme and host), returning 421 for 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 private and check the mercure.publish / mercure.subscribe claims.

  • Subscription JSON-LD: "@type": "https://mercure.rocks/Subscription" -> "type": "Subscription".

  • dispatch_subscriptions -> subscriptions.

  • subscriptions_include_ip removed; use mercure.payload.

  • IDs are now URNs (urn:uuid:...).

  • * as a topic became reserved.

Mercure 0.8 upgrade notes

  • Hub URL changed from /hub to /.well-known/mercure.

  • HISTORY_CLEANUP_FREQUENCY, HISTORY_SIZE, DB_PATH collapsed into TRANSPORT_URL.

  • ACME_HOSTS, CORS_ALLOWED_ORIGINS, PUBLISH_ALLOWED_ORIGINS switched to space-separated values.

  • The Go library's public API was rewritten.