Skip to main content
Technology & EngineeringRealtime Services296 lines

Mercure

Mercure is an open-source, self-hosted, real-time push API built on the W3C Server-Sent Events

Quick Summary3 lines
You are a real-time architect specializing in Mercure, the efficient server-sent events (SSE) hub. You leverage Mercure to build responsive web applications that react instantly to server-side changes, from chat notifications to live data dashboards. You understand its publish-subscribe model, the simplicity of SSE, and how to integrate it robustly into modern web stacks, ensuring secure, scalable, and low-latency data delivery.
skilldb get realtime-services-skills/MercureFull skill: 296 lines
Paste into your CLAUDE.md or agent config

Mercure Real-time Service

You are a real-time architect specializing in Mercure, the efficient server-sent events (SSE) hub. You leverage Mercure to build responsive web applications that react instantly to server-side changes, from chat notifications to live data dashboards. You understand its publish-subscribe model, the simplicity of SSE, and how to integrate it robustly into modern web stacks, ensuring secure, scalable, and low-latency data delivery.

Core Philosophy

Mercure's core philosophy centers on simplifying real-time, server-to-client data push using the ubiquitous Server-Sent Events (SSE) standard. Unlike WebSockets, which offer full-duplex communication, Mercure focuses on unidirectional data flow, making it ideal for scenarios where the server primarily needs to notify clients of updates (e.g., news feeds, stock tickers, notifications). This approach benefits from being built directly on HTTP, allowing existing infrastructure like proxies and load balancers to handle connections effectively.

You choose Mercure when your application primarily needs to send real-time updates from the server to many clients efficiently. It abstracts away the complexities of managing persistent connections and retransmissions inherent in raw SSE, providing a robust publish-subscribe hub. Its reliance on standard HTTP and JWT for authorization makes it straightforward to integrate into any web application stack, reducing development overhead compared to implementing a custom WebSocket server or polling mechanisms.

Setup

Setting up Mercure involves deploying the hub and configuring your application to publish updates to it.

1. Deploy the Mercure Hub

The easiest way to get Mercure running is via Docker.

# Pull the Mercure image
docker pull dunglas/mercure

# Run Mercure with a simple configuration
# M_URL: The public URL of your Mercure hub
# M_SERVER_NAME: Hostname for the server
# M_JWT_SECRET: A secret key to sign JWTs for authorization
# M_ALLOW_ANONYMOUS: Set to 1 to allow anonymous subscribers (for public topics)
docker run \
    -e M_URL="http://localhost:3000/.well-known/mercure" \
    -e M_SERVER_NAME=":3000" \
    -e M_JWT_SECRET="!ChangeMe!" \
    -e M_ALLOW_ANONYMOUS=1 \
    -p 3000:3000 \
    dunglas/mercure

Replace !ChangeMe! with a strong, production-ready secret. For production, you'll likely use a more advanced deployment (e.g., Kubernetes, systemd) and secure configuration.

2. Client-Side (JavaScript)

No specific SDK is required for the client; Mercure leverages the native EventSource API.

// In your main application JavaScript file
const mercureUrl = 'http://localhost:3000/.well-known/mercure';

// You might need to dynamically generate a subscription URL with topics and an authorization token
// For public topics, a simple URL works.
const eventSource = new EventSource(mercureUrl + '?topic=https://example.com/my-app/updates');

eventSource.onmessage = (event) => {
  // 'event.data' contains the payload published by your server
  console.log('Received update:', JSON.parse(event.data));
  // Example: Update UI element
  document.getElementById('live-data').innerText = event.data;
};

eventSource.onerror = (error) => {
  console.error('Mercure EventSource error:', error);
  // Handle reconnection logic or show user a message
};

eventSource.onopen = () => {
  console.log('Mercure connection opened.');
};

Key Techniques

1. Subscribing to Public Topics

Clients subscribe to public topics using a URL parameter. The Mercure hub pushes updates to any connected client that has subscribed to that topic.

// frontend/src/components/NotificationFeed.js
import React, { useEffect, useState } from 'react';

const NotificationFeed = () => {
  const [notifications, setNotifications] = useState([]);
  const mercureUrl = 'http://localhost:3000/.well-known/mercure';
  const publicTopic = 'https://example.com/my-app/notifications'; // Define your topic URL

  useEffect(() => {
    // Construct the EventSource URL with the desired topic
    const source = new EventSource(`${mercureUrl}?topic=${encodeURIComponent(publicTopic)}`);

    source.onmessage = (event) => {
      const newNotification = JSON.parse(event.data);
      setNotifications((prevNotifications) => [newNotification, ...prevNotifications]);
      console.log('New public notification:', newNotification);
    };

    source.onerror = (error) => {
      console.error('EventSource error:', error);
      source.close(); // Attempt to close and let the browser reconnect or handle manually
    };

    source.onopen = () => {
      console.log(`Subscribed to topic: ${publicTopic}`);
    };

    return () => {
      source.close(); // Clean up connection on component unmount
    };
  }, [publicTopic]);

  return (
    <div>
      <h2>Live Notifications</h2>
      <ul>
        {notifications.map((notif, index) => (
          <li key={index}>
            <strong>{notif.title}:</strong> {notif.message}
          </li>
        ))}
      </ul>
    </div>
  );
};

export default NotificationFeed;

2. Publishing Updates from Your Server

Your backend application publishes updates by sending a POST request to the Mercure hub's publish endpoint. This request must include a valid JWT in the Authorization header, generated using the M_JWT_SECRET.

// backend/src/services/mercurePublisher.js (Node.js example)
const jwt = require('jsonwebtoken');
const fetch = require('node-fetch'); // or axios

const MERCURE_HUB_URL = 'http://localhost:3000/.well-known/mercure';
const MERCURE_PUBLISH_SECRET = '!ChangeMe!'; // Must match M_JWT_SECRET in Mercure hub

// Function to generate a JWT for publishing
const generatePublishToken = () => {
  const payload = {
    mercure: {
      // The `publish` claim specifies which topics this token is authorized to publish to.
      // An empty array means all topics. For security, restrict this.
      publish: ['https://example.com/my-app/{id}', 'https://example.com/my-app/notifications']
    }
  };
  return jwt.sign(payload, MERCURE_PUBLISH_SECRET, { expiresIn: '1h' });
};

// Function to publish an update
const publishUpdate = async (topic, data, id = null, type = null) => {
  const token = generatePublishToken();

  const body = new URLSearchParams();
  body.append('topic', topic);
  body.append('data', JSON.stringify(data));
  if (id) body.append('id', id);
  if (type) body.append('type', type);

  try {
    const response = await fetch(MERCURE_HUB_URL, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Authorization': `Bearer ${token}`
      },
      body: body.toString()
    });

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`Failed to publish update: ${response.status} - ${errorText}`);
    }
    console.log(`Update published to topic "${topic}"`);
  } catch (error) {
    console.error('Error publishing to Mercure:', error.message);
  }
};

// Example usage:
// In your API route handler:
// app.post('/api/create-post', async (req, res) => {
//   const newPost = await createPost(req.body);
//   await publishUpdate('https://example.com/my-app/notifications', {
//     title: 'New Post Created!',
//     message: `A new post titled "${newPost.title}" has been published.`,
//     postId: newPost.id
//   });
//   res.status(201).json(newPost);
// });

module.exports = { publishUpdate };

3. Securing Private Topics with JWT (Client-side Subscription)

For user-specific or private updates, clients must provide a JWT with subscribe claims when connecting to Mercure. This JWT is typically generated by your backend and passed to the client.

// backend/src/auth/jwtGenerator.js (Node.js example)
const jwt = require('jsonwebtoken');

const MERCURE_SUBSCRIBE_SECRET = '!ChangeMe!'; // Same as M_JWT_SECRET in Mercure hub

const generateSubscribeToken = (userId) => {
  const payload = {
    mercure: {
      // The `subscribe` claim specifies which topics this token is authorized to subscribe to.
      // Use placeholders like {id} for dynamic topics.
      subscribe: [`https://example.com/my-app/users/${userId}/private-updates`, 'https://example.com/my-app/notifications']
    }
  };
  // Token lifetime should be relatively short (e.g., 5-15 minutes)
  return jwt.sign(payload, MERCURE_SUBSCRIBE_SECRET, { expiresIn: '15m' });
};

// In your user login/session endpoint:
// app.post('/api/login', async (req, res) => {
//   const user = await authenticateUser(req.body.username, req.body.password);
//   if (user) {
//     const subscribeToken = generateSubscribeToken(user.id);
//     res.json({ user, subscribeToken }); // Send token to client
//   } else {
//     res.status(401).json({ message: 'Invalid credentials' });
//   }
// });

module.exports = { generateSubscribeToken };
// frontend/src/components/UserDashboard.js (React example)
import React, { useEffect, useState } from 'react';

const UserDashboard = ({ userId, subscribeToken }) => {
  const [userUpdates, setUserUpdates] = useState([]);
  const mercureUrl = 'http://localhost:3000/.well-known/mercure';
  const userSpecificTopic = `https://example.com/my-app/users/${userId}/private-updates`;

  useEffect(() => {
    if (!subscribeToken || !userId) return;

    // Append the authorization token to the Mercure URL
    // The Mercure hub will validate this token for subscription to the specified topics.
    const source = new EventSource(
      `${mercureUrl}?topic=${encodeURIComponent(userSpecificTopic)}&Authorization=Bearer%20${subscribeToken}`
    );

    source.onmessage = (event) => {
      const update = JSON.parse(event.data);
      setUserUpdates((prevUpdates) => [update, ...prevUpdates]);
      console.log('New private update:', update);
    };

    source.onerror = (error) => {
      console.error('Private EventSource error:', error);
      source.close();
    };

    source.onopen = () => {
      console.log(`Subscribed to private topic for user ${userId}`);
    };

    return () => {
      source.close();
    };
  }, [userId, subscribeToken, userSpecificTopic]);

  return (
    <div>
      <h2>My Private Dashboard</h2>
      <ul>
        {userUpdates.map((update, index) => (
          <li key={index}>
            <strong>{update.type}:</strong> {update.message}
          </li>
        ))}
      </ul>
## Anti-Patterns

**Using the service without understanding its pricing model.** Cloud services bill differently — per request, per GB, per seat. Deploying without modeling expected costs leads to surprise invoices.

**Hardcoding configuration instead of using environment variables.** API keys, endpoints, and feature flags change between environments. Hardcoded values break deployments and leak secrets.

**Ignoring the service's rate limits and quotas.** Every external API has throughput limits. Failing to implement backoff, queuing, or caching results in dropped requests under load.

**Treating the service as always available.** External services go down. Without circuit breakers, fallbacks, or graceful degradation, a third-party outage becomes your outage.

**Coupling your architecture to a single provider's API.** Building directly against provider-specific interfaces makes migration painful. Wrap external services in thin adapter layers.

Install this skill directly: skilldb add realtime-services-skills

Get CLI access →