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

Encryption

HTTPS protects data on the wire. It does not protect data from the hub. Operators of a Mercure hub can read every update that flows through it. For most cases that's fine; the hub is your infrastructure.

When it isn't (third-party hub, multi-tenant hub, regulated data), encrypt the payload end-to-end with JSON Web Encryption.

How Mercure end-to-end encryption works

# How Mercure End-to-End Encryption Works
publisher                    hub                       subscriber
    |                          |                           |
    |  encrypt(data, K)        |                           |
    | -----------------------> |  (sees ciphertext only)   |
    |                          | ------------------------> |  decrypt(ciphertext, K)

The publisher encrypts the data field before posting it. The hub stores and forwards the ciphertext like any other payload. The subscriber decrypts it after receiving the SSE event.

The hub never sees the plaintext, the key, or anything that lets it derive the key.

Distributing the JWE key between publisher and subscriber

The publisher and the subscriber need a shared key. Two patterns work:

1. Out-of-band. The publisher and the subscriber both fetch the key from a side channel (your own API, a vault). The hub doesn't see the request.

2. Through the discovery endpoint. When the publisher controls the resource the subscriber fetches first, attach a key-set link to the discovery response:

# Distributing the JWE Key Between Publisher and Subscriber
GET /books/1
Host: example.com
Authorization: Bearer <session token>

200 OK
Link: <https://hub.example.com/.well-known/mercure>; rel="mercure"
Link: <https://example.com/keys/books/1>; rel="key-set"
Content-Type: application/ld+json

{ "@id": "/books/1", "...": "..." }

The key-set URL must serve JWK Set JSON. Authorize the request. Anyone who reads the key set can decrypt the updates. Reuse the same auth your application already enforces.

Publishing encrypted Mercure updates

// Publishing Encrypted Mercure Updates
import { CompactEncrypt } from "jose";

const key = /* the JWK */;
const plaintext = JSON.stringify({ status: "checked out" });

const jwe = await new CompactEncrypt(new TextEncoder().encode(plaintext))
  .setProtectedHeader({ alg: "RSA-OAEP-256", enc: "A256GCM" })
  .encrypt(key);

await fetch("https://hub.example.com/.well-known/mercure", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${jwt}`,
    "Content-Type": "application/x-www-form-urlencoded",
  },
  body: new URLSearchParams({
    topic: "https://example.com/books/1",
    data: jwe,
  }),
});

Decrypting on the subscriber

In a browser, with jose:

// Decrypting on the subscriber
import { compactDecrypt } from "jose";

const key = await fetchKey(); // from the key-set link
const es = new EventSource(url);
es.onmessage = async (event) => {
  const { plaintext } = await compactDecrypt(event.data, key);
  const update = JSON.parse(new TextDecoder().decode(plaintext));
  // ...
};

The browser's WebCrypto API can do the same without an external library if you only need a fixed algorithm.

What you give up

End-to-end encryption rules out a few hub-side conveniences:

  • Server-side filtering on payload contents. The hub can't introspect the data, so any matching has to be on the topic.

  • Server-rendered debug UI. The debugger UI shows ciphertext, not plaintext.

  • Partial updates that depend on aggregating prior state at the hub. Any aggregation has to happen on the subscriber.

The topic and the SSE metadata (event ID, type) are still visible to the hub. If you need to hide those too, derive opaque topics from a secret and rotate them.

When to encrypt Mercure updates with JWE

End-to-end encryption is the right call when:

  • The hub is operated by a different org than the publisher (multi-tenant SaaS, third-party deployment).

  • The data is subject to regulation (GDPR sensitive categories, HIPAA, financial data) and you can't argue that the hub processor is in scope.

  • You're moving to Cloud but a subset of topics carries data you'd rather not have us see.

If the hub is yours and you control its hosts, HTTPS is sufficient and JWE adds operational cost without a meaningful security gain.

Not a substitute for authorization

Encryption hides content; authorization controls who connects. You still need the JWT layer on top: to keep unauthorized clients off the hub, and to gate private updates so the hub knows who to deliver them to even when it can't read them.

For full data residency and operator control without the encryption overhead, Self-Hosted Mercure runs the same hub on your own infrastructure with no third party in the path.