Skip to content

Configuration

The Mercure.rocks Hub is a Caddy build with the Mercure module. Anything the Caddy docs describe also applies to this binary.

The most idiomatic way to configure it is a Caddyfile. Other formats (JSON, the admin API, env-var-driven config) work too; Mercure ships pre-wired for env vars in the official Docker image.

#Minimal Caddyfile

# Caddyfile
hub.example.com {
  mercure {
    publisher_jwt  {env.MERCURE_PUBLISHER_JWT_KEY}
    subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY}
    cors_origins   https://example.com
  }

  respond "Not Found" 404
}

Caddy provisions a Let's Encrypt certificate for hub.example.com automatically. To disable HTTPS (when behind a reverse proxy that terminates TLS), prefix the site address with http://:

# Caddyfile
http://hub.example.com:80 {
  # ...
}

Setting the port to 80 also disables HTTPS implicitly.

#Mercure directives

DirectiveDescriptionDefault
publisher_jwt <key> [<algorithm>]JWT key + algorithm for publishers. Supports Caddy placeholders.
subscriber_jwt <key> [<algorithm>]JWT key + algorithm for subscribers.
publisher_jwks_url <url>JWK Set URL for publisher token validation. Takes precedence over publisher_jwt.
subscriber_jwks_url <url>JWK Set URL for subscriber token validation.
anonymousAllow subscribers without a JWT to receive public updates.off
publish_origins <origin...>Origins allowed to publish (cookie-based auth only).
cors_origins <origin...>CORS allowed origins. See CORS.
cookie_name <name>Cookie that carries the JWT for browser clients.mercureAuthorization
subscriptionsEnable subscription events and the subscription API.off
heartbeat <duration>Interval between SSE heartbeat comments. 0s to disable.40s
transport <name> [{ <options...> }]Transport configuration. See Transports.bolt
dispatch_timeout <duration>Max time to dispatch one update to one subscriber. 0s disables.5s
write_timeout <duration>Max duration of a subscriber connection. 0s disables. See Rolling updates.600s
topic_selector_cache <maxEntries> [<shards>]Cache for matcher evaluations. -1 to disable, 0 for unbounded.10000 256
subscriber_list_cache_size <maxSize>Subscriber list cache size. 0 for unbounded.100000
demoEnable the debug UI and demo endpoints. Dev only.off
uiEnable the debug UI without the demo endpoints.off

The directives marked dev-only (demo, ui, anonymous) are off by default in production. Don't enable them on a hub that serves real users.

#Mercure hub environment variables

The Docker image and the official Caddyfile read these:

VariableDescriptionDefault
SERVER_NAMESite address. Use :80 to bind without a hostname.localhost
MERCURE_PUBLISHER_JWT_KEYPublisher signing key.
MERCURE_PUBLISHER_JWT_ALGPublisher algorithm.HS256
MERCURE_SUBSCRIBER_JWT_KEYSubscriber signing key.
MERCURE_SUBSCRIBER_JWT_ALGSubscriber algorithm.HS256
MERCURE_EXTRA_DIRECTIVESAdditional Mercure directives. One per line.
GLOBAL_OPTIONSCaddy global options.
CADDY_EXTRA_CONFIGSnippets / named routes.
CADDY_SERVER_EXTRA_DIRECTIVESCaddyfile directives outside the mercure block.
MERCURE_LICENSELicense key for Self-Hosted Mercure.

MERCURE_EXTRA_DIRECTIVES is convenient for quick tweaks but don't put credentials there (transport passwords, JWKS URLs with tokens). Write a custom Caddyfile and use {env.MY_SECRET} for those.

#Mercure hub transports

The transport stores history and (in clustered builds) synchronizes between nodes.

#Bolt transport (default, single-node)

# Bolt transport (default, single-node)
mercure {
  transport bolt {
    path /data/mercure.db
    size 0
    cleanup_frequency 0.3
  }
  # ...
}
OptionDescription
pathPath to the BoltDB file. Default: mercure.db.
bucket_nameBucket name. Default: updates.
cleanup_frequencyProbability per publish of running history cleanup. 0 (never) to 1 (always).
sizeMaximum number of events to keep. 0 for unlimited (default; bound only by disk size).

The open-source build keeps history forever by default. Set size if you want a cap.

#Local transport (no history)

transport local disables history entirely. Use it when reconnect replay isn't needed and you want the lowest possible memory footprint.

#Redis / Postgres / Kafka / Pulsar

These ship with Self-Hosted Mercure. They enable multi-node deployments and queryable history.

Pro tip. The open-source hub runs on a single node. For redundancy across nodes, low-latency multi-region deploys, or storing events in Redis or Postgres for SQL-backed queries, Self-Hosted Mercure ships those transports starting at €1,500/year.

#CORS

If the page that opens the SSE connection is on a different origin than the hub, you must list it in cors_origins:

# CORS
mercure {
  cors_origins https://app.example.com https://admin.example.com
}

* is allowed only if the hub is fully anonymous (no JWT, no cookie). Browsers refuse credentialed requests from a wildcard origin.

If your app and hub run on the same registrable domain (e.g. example.com and hub.example.com), the hub can be reached without CORS at all by going through a reverse proxy that mounts the hub on the app's origin. See Reverse proxies.

#JWT validation via JWKS

When tokens are minted by an external IdP (Keycloak, Cognito, Auth0):

# JWT validation via JWKS
mercure {
  publisher_jwks_url https://idp.example.com/.well-known/jwks.json
  subscriber_jwks_url https://idp.example.com/.well-known/jwks.json
}

The hub fetches and caches the keys, validates each token's kid against them, and rotates automatically when the IdP rotates. Token issuance stays with the IdP; the hub only verifies.

#RSA / ECDSA keys

# RSA / ECDSA keys
ssh-keygen -t rsa -b 4096 -m PEM -f publisher.key
openssl rsa -in publisher.key -pubout -outform PEM -out publisher.key.pub

Start the hub with the public key for verification and the algorithm:

# RSA / ECDSA keys
MERCURE_PUBLISHER_JWT_KEY="$(cat publisher.key.pub)" \
MERCURE_PUBLISHER_JWT_ALG=RS256 \
MERCURE_SUBSCRIBER_JWT_KEY="$(cat subscriber.key.pub)" \
MERCURE_SUBSCRIBER_JWT_ALG=RS256 \
./mercure run

#Mercure hub health check endpoints

The Caddy admin API (default localhost:2019) exposes:

EndpointDescription
GET /mercure/health/ready200 if all transports can serve traffic, 503 otherwise.
GET /mercure/health/live200 if all transports are fundamentally operational.
GET /mercure/health/{name}/readyPer-hub readiness (when running multiple).
GET /mercure/health/{name}/livePer-hub liveness.

The endpoints bind to localhost for security. Probes from outside the container should use kubectl exec or docker exec (see Health monitoring). Binding the admin API to 0.0.0.0:2019 works but exposes /stop and /load to the pod network. Almost never what you want.

#Mercure hub performance tuning

A few knobs that move the needle:

  • dispatch_timeout: too low and slow subscribers get cut off; too high and a stuck dispatch ties up resources. The 5s default is a reasonable starting point.
  • write_timeout: controls how often each subscriber rotates its connection in steady state. Higher values mean fewer reconnects but worse drain pacing on shutdown. See Rolling updates.
  • topic_selector_cache and subscriber_list_cache_size: increase if your hub has many distinct matchers and you see CPU spent in matcher evaluation. Decrease if memory is tight.
  • File descriptors: every subscriber takes one. ulimit -n 100000 on the host (or the equivalent in your orchestrator) for high-fanout hubs.

Load testing and Debugging cover the rest.

#Mercure hub configuration reload

Caddy hot-reloads on signal: kill -USR1 <pid> or caddy reload. Active SSE connections are preserved across reloads as long as the listening sockets don't change.

#Mercure hub runtime introspection

The Caddy admin API also exposes:

  • /config/: the current effective config (JSON).
  • /metrics: Prometheus metrics (when metrics is in GLOBAL_OPTIONS).
  • /debug/pprof/: Go profiler endpoints (when debug is in GLOBAL_OPTIONS). See Debugging.