- Introduction à Mercure
- Architecture du système
- Installation et configuration de Mercure
- Configuration Apache et SSL
- Génération des clés JWT
- Exemple d'implémentation backend
- Exemple d'implémentation frontend
1. Introduction à Mercure
Mercure est un protocole de communication temps réel basé sur les Server-Sent Events (SSE). Contrairement à WebSockets, Mercure utilise le protocole HTTP standard, ce qui le rend plus simple à implémenter et compatible avec le cache HTTP.
Avantages de Mercure
- Simple : Utilise HTTP/1.1 et HTTP/2
- Économe : Moins de ressources que WebSockets
- Compatibilité : Fonctionne derrière les proxies
- Reconnexion automatique : Gère les déconnexions
- Support natif dans la majorité des navigateurs
2. Architecture du système

- Le client (navigateur, app mobile, etc.) ouvre une connexion
HTTP/HTTPSvers le hub Mercure (via le domaine géré par le vhost Apache, ex :messages.habeuk.com). - Le vhost Apache joue uniquement le rôle de proxy : il redirige la requête vers le conteneur Mercure (souvent sur
127.0.0.1:3000). - L’application Symfony publie un message sur Mercure (via le
HubInterfaceet unUpdate). - Le hub Mercure envoie ensuite ce message en temps réel à tous les clients abonnés au topic concerné (via la connexion SSE ouverte au début).
3. Installation et configuration de Mercure
3.1. Configuration Docker Compose
Créer un dossier ou vous souhaitez gérer le serveur mercure, (exemple 'mercure_habeuk'), accéder à ce dossier et créer le fichier de configuration de mercure (docker-compose.yml).
# compose.yaml
services:
mercure:
image: dunglas/mercure
restart: unless-stopped
container_name: mercure_habeuk
environment:
# Au niveau de mercure on ecoute sur le port 80.( mercure est dans un docker ).
SERVER_NAME: ':80'
MERCURE_PUBLISHER_JWT_KEY: "${MERCURE_PUBLISHER_JWT_KEY}"
MERCURE_SUBSCRIBER_JWT_KEY: "${MERCURE_SUBSCRIBER_JWT_KEY}"
MERCURE_EXTRA_DIRECTIVES: |
cors_origins https://www.habeuk.com https://habeuk.com
anonymous
ports:
- "127.0.0.1:3000:80"
# configuration avec volumes nommés gerer par docker ( /var/lib/docker/volumes/... )
volumes:
- mercure_data:/data
- mercure_config:/config
volumes:
mercure_data:
mercure_config:Explications :
SERVER_NAME: ':80' → le chiffre 80 indique le port sur lequel le serveur Mercure écoutera à l’intérieur du conteneur Docker. Comme Mercure tourne dans un conteneur isolé, ce port ne rentre pas en conflit avec celui de la machine hôte. On aurait d’ailleurs pu choisir un autre port interne si nécessaire.
MERCURE_PUBLISHER_JWT_KEY: "${MERCURE_PUBLISHER_JWT_KEY}" et MERCURE_SUBSCRIBER_JWT_KEY: "${MERCURE_SUBSCRIBER_JWT_KEY}" → ce sont les clés JWT utilisées respectivement pour publier et souscrire aux événements Mercure. Elles sont définies dans le fichier d’environnement mercure_habeuk/.env.
MERCURE_EXTRA_DIRECTIVES → permet d’ajouter des instructions supplémentaires à la configuration du hub Mercure (c’est comme un petit bloc de configuration interne).
cors_origins https://www.habeuk.com https://habeuk.com → définit les origines autorisées (CORS) qui peuvent se connecter au hub Mercure. Cela signifie que seules les pages web servies depuis https://www.habeuk.com et https://habeuk.com auront le droit :
- de s’abonner aux événements (via EventSource) ;
- et d’envoyer des requêtes de publication (si les droits JWT le permettent).
anonymous → autorise les connexions sans jeton JWT pour la souscription. En d’autres termes, les clients peuvent s’abonner à certains topics sans avoir besoin de s’authentifier, ce qui est utile pour les flux publics (comme des messages visibles par tous ou des notifications globales).
ports:127.0.0.1:3000:80 → indique à Docker de rediriger le port 80 du conteneur Mercure vers le port 3000 de la machine hôte, mais uniquement accessible depuis 127.0.0.1 (localhost).
En d’autres termes : - Le service Mercure écoute sur le port 80 à l’intérieur du conteneur. - Ce port est exposé localement sur le port 3000 de la machine hôte. - L’adresse 127.0.0.1 limite l’accès à la machine locale uniquement (personne ne peut atteindre le port 3000 depuis l’extérieur).
3.2. Démarrage du service
installation et démarrage de mercure
docker compose up -darrêter et redémarrer le service
docker compose down && docker compose up -d3.2. Test du service Mercure
On vérifie en premier si le serveur mercure fonctionne sur l'@ ip et le port définit. (directement sur le VPS)
curl http://127.0.0.1:3000/.well-known/mercure?topic=testSi tout est correct, la commande doit ouvrir une connexion SSE.
Nous souhaitons que le serveur mercure puisse être accessible via ce sous domaine messages.habeuk.com.
On modifier le fichier /etc/hosts en y ajoutant ce qui suit:
127.0.0.1 messages.habeuk.comPuis on effectue un autre test
curl http://messages.habeuk.com:3000/.well-known/mercure?topic=testSi tout est correct, la commande doit ouvrir une connexion SSE.
4. Configuration Apache et SSL
Nous souhaitons une adresse propre, c'est-à-dire messages.habeuk.com sans le numéro de port. Apache va intervenir afin de transférer les requêtes reçues sur messages.habeuk.com vers l'adresse 127.0.0.1:3000 de manière transparente pour l'utilisateur.
Nous devons ajouter un virtual host dans /etc/apache2/sites-available/messages.habeuk.com.conf :
<VirtualHost *:80>
ServerName messages.habeuk.com
# Proxy toutes les requêtes HTTP vers Mercure (port 3000)
ProxyPreserveHost On
ProxyRequests Off
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
#
ErrorLog /var/www/habeuk/logs/error-mercure.log
CustomLog /var/www/habeuk/logs/access-mercure.log combined
</VirtualHost>Activation des modules Apache 2 :
sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod headers
sudo a2enmod rewriteActivation du virtual host :
sudo a2ensite messages.habeuk.com.confRedémarrage d'Apache 2 :
sudo systemctl restart apache2Ensuite, on effectue un test :
curl http://messages.habeuk.com/.well-known/mercure?topic=testSi tout est correct, la commande doit ouvrir une connexion SSE.
Vous pouvez aussi vérifier directement dans votre navigateur : http://messages.habeuk.com doit afficher la page d'accueil de Mercure.
Activez le SSL sur votre sous-domaine messages.habeuk.com en suivant cette documentation.
Mettez à jour le fichier virtual host /etc/apache2/sites-available/messages.habeuk.com.conf :
<VirtualHost *:80>
ServerName messages.habeuk.com
Redirect permanent / https://messages.habeuk.com/
</VirtualHost>
<VirtualHost *:443>
ServerName messages.habeuk.com
# Proxy toutes les requêtes HTTPS vers Mercure (port 3000)
ProxyPreserveHost On
ProxyRequests Off
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
#
ErrorLog /var/www/habeuk/logs/error-mercure.log
CustomLog /var/www/habeuk/logs/access-mercure.log combined
# Activation SSL
SSLEngine On
SSLCertificateFile /etc/letsencrypt/live/habeuk.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/habeuk.com/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>Enfin, refaites le test précédent, cette fois avec HTTPS :https://messages.habeuk.com
5. Génération des clés JWT
La sécurité de Mercure repose sur des JWT (JSON Web Tokens) signés avec des clés cryptographiques. Générez des secrets aléatoires suffisamment longs à l'aide de la commande suivante :
openssl rand -base64 32Créez ensuite le fichier mercure_habeuk/.env et ajoutez-y le contenu suivant :
MERCURE_PUBLISHER_JWT_KEY=your-secure-publisher-key-from-openssl
MERCURE_SUBSCRIBER_JWT_KEY=your-secure-subscriber-key-from-openssl 6. Exemple d’implémentation backend (via symfony)
<?php
// src/Service/ChatService.php
namespace App\Service;
use App\Entity\ {
Notification,
NotificationReceipt
};
use App\Enum\NotificationType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
use Psr\Log\LoggerInterface;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
class SendNotificationByMercureService {
public function __construct(private EntityManagerInterface $em, private HubInterface $hub, private SerializerInterface $serializer, private LoggerInterface $logger) {
}
public function publishViaMercure(Notification $notification): void {
$receipts = $notification->getReceipts();
$topics = [];
if ($receipts->count() > 0) {
foreach ($receipts->getValues() as $receipt) {
/**
*
* @var NotificationReceipt $receipt
*/
$topics[] = 'notification/' . $receipt->getRecipient()->getId();
}
try {
$result = $this->serializer->serialize($notification, 'json',
[
AbstractNormalizer::GROUPS => [
'notification:read'
],
AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object) {
return $object->getId();
}
]);
$type = $notification->getType() == NotificationType::NEW_MESSAGE ? "new_message" : "notification";
$update = new Update($topics, $result, true, null, $type);
$this->hub->publish($update);
}
catch (\Exception $e) {
$this->logger->error($e->getMessage());
}
}
}
}7. Exemple d’implémentation frontend
// mercureClient.js
class MercureClient {
constructor(url) {
this.eventSource = new EventSource(url, { withCredentials: true });
this.eventSource.onerror = (err) => {
console.error("Mercure connection error:", err);
};
}
build() {
const events = ["chat", "read_message", "notification", "new_message"];
events.forEach((event_name) => {
this.Listerner(event_name);
});
this.eventSource.onmessage = (event) => {
console.log("MercureClient event onmessage : ", event);
alert("ezsd");
};
}
Listerner(event_name) {
this.eventSource.addEventListener(event_name, (event) => {
const data = JSON.parse(event.data);
this.dispatchCustomEvent(event_name, data);
});
}
dispatchCustomEvent(event_name, data) {
console.log("dispatchCustomEvent event_name : ", event_name, data);
document.dispatchEvent(
new CustomEvent(`mercure-${event_name}`, {
detail: data,
})
);
}
}
export default MercureClient;