Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Mercure Notifier Laravel Package

symfony/mercure-notifier

Symfony Mercure Notifier bridge integrates Mercure with the Symfony Notifier component, enabling real-time push notifications through a Mercure hub. Configure a Mercure transport to deliver notifier messages to browsers and clients.

Deep Wiki
Context7

Getting Started

This package is thin glue, not a notifier in its own right: it adds a mercure:// transport to symfony/notifier that publishes notifications to a Mercure hub as SSE updates. In practice you spend 10% of your time here and 90% in Mercure configuration and frontend subscription code.

Minimal setup

composer require symfony/mercure-notifier symfony/mercure-bundle
# config/packages/mercure.yaml
mercure:
    hubs:
        default:
            url: '%env(MERCURE_URL)%'          # server-side publish endpoint
            public_url: '%env(MERCURE_PUBLIC_URL)%'  # browser-facing hub
            jwt:
                secret: '%env(MERCURE_JWT_SECRET)%'
# config/packages/notifier.yaml
framework:
    notifier:
        chatter_transports:
            mercure: 'mercure://default'   # "default" = hub name above

First real use case — push a live in-app notification to one user's browser:

use Symfony\Component\Notifier\Bridge\Mercure\MercureMessage;
use Symfony\Component\Notifier\ChatterInterface;

final class LiveNotifier
{
    public function __construct(private ChatterInterface $chatter) {}

    public function notify(int $userId, string $title, string $body): void
    {
        $this->chatter->send(
            (new MercureMessage())
                ->subject($title)
                ->content(json_encode(['title' => $title, 'body' => $body]))
                ->topic(sprintf('https://example.com/users/%d/notifications', $userId))
        );
    }
}

Browser side:

const es = new EventSource(
  `${MERCURE_PUBLIC_URL}?topic=${encodeURIComponent(`https://example.com/users/${userId}/notifications`)}`,
  { withCredentials: true }
);
es.onmessage = (e) => toast(JSON.parse(e.data));

Where to look first

  1. vendor/symfony/mercure-notifier/MercureTransport.php — it's ~40 lines; read it, then MercureTransportFactory.php. Everything the package does is there.
  2. The Symfony docs page "Mercure Notifier" (one screen).
  3. config/packages/mercure.yaml and .env — most failures are MERCURE_URL / JWT config, not code.
  4. If your message class doesn't match, the LogicException thrown in doSend() tells you exactly what type the transport accepts.

Implementation Patterns

Direct chatter vs. NotifierInterface. The clean path is ChatterInterface + a MercureMessage, because the message carries the topic and update options. Going through NotifierInterface::send(new Notification(...), ['mercure']) builds a generic ChatMessage, which the Mercure transport may reject or publish without your intended topic. If you want the high-level Notification API, write a tiny custom channel that converts to MercureMessage.

Per-user private topics (the default for anything user-specific):

$message
    ->topic('https://example.com/users/'.$user->getId().'/notifications')
    ->options((new MercureOptions())->private(true));

Then mint a subscriber JWT containing mercure.subscribe for that topic and set it as a cookie (mercureAuthorization) — otherwise the browser connects successfully but receives nothing, which is the single most common "it doesn't work" report. The bridge exposes MercureOptions for the Mercure update flags (private, id, type, retry); verify the setter names against the version you installed, as they track symfony/mercure.

Public broadcast for announcements/global toasts: omit private, use a stable IRI topic like https://example.com/announcements, and let any client subscribe unauthenticated.

JSON envelope convention. Mercure carries a string. Pick one shape early and standardize on it across all publishers:

{ "type": "order.shipped", "title": "Shipped", "body": "...", "url": "/orders/42" }

Frontend switches on type. This avoids a rewrite when you add a second notification source.

Async publishing. HubInterface::publish() is a synchronous HTTP call to the hub. In request paths, dispatch a NotificationMessage through Messenger and publish in the handler. Keeps response times flat and gives you retries for hub blips.

Domain-event fan-out. Rather than sprinkling ChatterInterface calls, listen to domain events (OrderShipped, CommentMentioned) and translate them to Mercure messages in one listener. You get one place to change topic conventions and payload shapes.

Reconnection. Use Last-Event-ID and the id update option for anything where missing a message matters; EventSource reconnects automatically and replays from that ID only if the hub kept history (it does not by default — see gotchas).

Testing. Mock Symfony\Component\Mercure\HubInterface and assert publish() received an Update with the expected topic and data. The bridge ships a transport-factory test case; extending it is the fastest way to keep DSN parsing covered. In functional tests, either swap the hub for a fake or assert on the fake's captured updates.

Gotchas and Tips

  • Don't read the star count. 16 stars is a read-only split of the Symfony monorepo; the code ships in millions of Symfony installs. Judge it by the parent framework's release cadence (releases track Symfony minor versions), not the repo.
  • MERCURE_URL vs MERCURE_PUBLIC_URL. The notifier publishes to url (internal, e.g. http://hub:3000/.well-known/mercure); browsers connect to public_url. Swapping them produces either connection timeouts server-side or mixed-content/CORS failures client-side.
  • JWT is two different things. Publishing needs a JWT with mercure.publish (usually a secret on the mercure bundle config); subscribing to private topics needs a JWT with mercure.subscribe for the exact topic IRIs, delivered as a cookie. Getting the first right and the second wrong yields a silent empty stream.
  • Topic matching is exact string matching on IRIs. https://example.com/users/1/notifications and .../users/1/Notifications and .../users/01/... are different topics. Build them with one helper, not inline sprintf in five places.
  • Fire-and-forget by design. Mercure does not persist missed updates. Offline users, closed tabs, and hub restarts lose messages. If delivery matters, write the notification to your database first and let the client fetch unread items on reconnect — treat Mercure as the "latency optimization" layer, not the source of truth.
  • Browser connection limits. HTTP/1.1 caps you at ~6 connections per origin; multiple EventSource instances starve each other and the page. Use HTTP/2, or open one connection and subscribe to multiple topics via repeated topic= query params.
  • SSE event type vs onmessage. If you set a custom type on the update, it fires as a named event (es.addEventListener('order.shipped', ...)), and onmessage never fires. Choose one convention.
  • Hub name resolution. mercure://default resolves a hub from mercure.hub.default via the Mercure bundle's registry. In a non-full-stack app (no bundle), you must register MercureTransportFactory yourself with the right HubInterface; the DSN alone does nothing.
  • getMessageId() is null. Mercure doesn't return a publish ID, so SentMessage::getMessageId() is empty. Don't build idempotency or logging around it.
  • Exceptions are rewrapped. A hub 401/500 surfaces as a Notifier TransportException, hiding the HTTP detail. Catch it and inspect the previous exception, or log the hub response at the Mercure client level.
  • Keep Symfony packages in lockstep. symfony/mercure-notifier, symfony/notifier, and symfony/mercure are released together; version skew between them is the usual source of "method does not exist" surprises after composer update.
  • Debugging checklist: bin/console debug:container mercure.hub.default to confirm the hub wiring; bin/console debug:container --tag=notifier.transport_factory to confirm the transport is registered; then curl -X POST to the hub with a hand-made JWT to isolate whether the problem is Symfony or Mercure. There is no debug:notifier command.
  • CORS. The hub must allow your app's origin for the browser connection (CORS_ALLOWED_ORIGINS on the Mercure container). Server-side publishing works fine while the browser silently fails with a CORS error only visible in devtools.
  • Payload size. Every update is buffered and streamed as text; don't push whole entities. Publish an ID and let the client fetch details over HTTP.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr
laravel2580/idehelpers
delatbabel/viewpages
anavel/gettext
muhamadrezaar/highcharts
delatbabel/keylists
frankkessler/salesforce-laravel-oauth2-rest
anavel/foundation
wdmtech/enviricons
eii/laravel-captcha
muka/shape-reader
tdt/json