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

Laravel Mercure Broadcaster Laravel Package

mvanduijker/laravel-mercure-broadcaster

A Laravel broadcaster that sends your app's events to a Mercure hub for real-time updates over Server-Sent Events. Skip Pusher and websockets: publish model changes and notifications to browsers with a simple, lightweight setup that plugs into Laravel's existing broadcasting system.

View on GitHub
Deep Wiki
Context7

Getting Started

This package is the publish-side driver for a Mercure hub: it teaches Laravel's broadcaster how to POST updates to the hub and how to hand authenticated subscribers a JWT. It is not a hub — you still run one (Docker dunglas/mercure, or the Caddy-based binary).

Install and wire up:

composer require mvanduijker/laravel-mercure-broadcaster
php artisan vendor:publish --tag=mercure   # -> config/mercure.php

Then set at minimum:

  • MERCURE_URL — the server-to-hub publish endpoint, e.g. http://mercure:80/.well-known/mercure (can be an internal Docker hostname).
  • MERCURE_PUBLIC_URL — the browser-facing endpoint; must be absolute and reachable by the client.
  • MERCURE_JWT (or the signing secret variant) — a token carrying at least mercure.publish.

Register the connection and make it the default:

// config/broadcasting.php
'connections' => [
    'mercure' => ['driver' => 'mercure'],
],
BROADCAST_CONNECTION=mercure

Where to look first: config/mercure.php (the shipped keys are the source of truth for env names — confirm whether your version wants a pre-signed token or a secret to sign with), then the package source for MercureBroadcaster — specifically auth()/validAuthenticationResponse(), which is where the subscriber cookie is set.

First use case — a private per-user feed. Create an event implementing ShouldBroadcastNow, publish it, and subscribe with a plain EventSource (not Echo — see Gotchas).

On 3.9.0: this is a compatibility-only release (Laravel 13.x added to the constraint). Nothing to migrate; treat it as a reason to re-run your app's test suite on the new framework minor, not as a feature to adopt.

Implementation Patterns

Prefer ShouldBroadcastNow for anything user-visible. Mercure is an HTTP push; routing it through a queue adds latency for no benefit. Reserve ShouldBroadcast for high-volume, non-interactive streams.

class OrderShipped implements ShouldBroadcastNow
{
    public function __construct(public Order $order) {}

    public function broadcastOn(): array
    {
        return [new PrivateChannel('orders.'.$this->order->id)];
    }
}

Use PrivateChannel as the default. Mercure has no presence concept, and a public Channel is readable by anyone who knows the topic IRI. Private channels force a subscribe JWT, which the package's auth() mints after your routes/channels.php callback authorizes the user.

Wire authorization explicitly. On Laravel 11+:

// bootstrap/app.php
->withBroadcasting(__DIR__.'/../routes/channels.php', [
    'middleware' => ['web', 'auth'],
])

The auth response sets an HTTP-only cookie, so the browser needs withCredentials:

const url = new URL(mercurePublicUrl);
url.searchParams.append('topic', orderTopic); // must equal the published IRI
const es = new EventSource(url, { withCredentials: true });
es.onmessage = (e) => handle(JSON.parse(e.data));

Ad-hoc publishes belong in an event class, not in a controller. If you need to push something that isn't modelled as a domain event, write a tiny "notification" event with ShouldBroadcastNow rather than digging for a hub client — it keeps topic derivation in one place.

Centralise topic construction. Generate the topic IRI from the same expression on both sides (a small helper or a model method), because the browser and the broadcaster must agree byte-for-byte.

Multi-topic subscriptions are just repeated topic query params — useful for a dashboard that watches several private channels with one connection.

Testing: assert at the event layer (Event::fake() + assertDispatched) rather than letting the hub call go out. For an end-to-end check, point MERCURE_URL at a local hub container and use curl -N against the public URL.

Gotchas and Tips

  • MERCURE_URL vs MERCURE_PUBLIC_URL is the #1 failure mode. If they disagree on scheme/host/port, publishing succeeds, SSE connects, and nothing ever arrives. The hub doesn't error — it just has no matching subscriber.
  • Topic IRIs are exact-match. Trailing slashes, http vs https, localhost vs 127.0.0.1, or a non-default port will silently break delivery. Echo the topic from JS and Log::info() the one the event produces; diff them once and you'll never debug it again.
  • JWT claims are directional. Publishing needs mercure.publish; subscribing needs mercure.subscribe with the topic in the claim (or *). A 401 from the hub is almost always the token, not Laravel auth.
  • Cookie name must match the hub's configuration, and cross-origin setups need SameSite/Secure set correctly plus CORS on the hub — otherwise the cookie is never sent and every private channel 401s.
  • Don't reach for Laravel Echo. Echo speaks the Pusher protocol; Mercure is plain SSE. This trips people coming from Reverb/Pusher and produces a lot of wasted time. (php artisan install:broadcasting will scaffold Pusher/Reverb config you should discard.)
  • BROADCAST_CONNECTION is the Laravel 11+ name; older docs and blog posts say BROADCAST_DRIVER. Setting the wrong one leaves you on the log driver with no error.
  • Config cache. php artisan config:clear after touching .env — a cached mercure.php survives env changes and looks like a broken hub.
  • No replay by default. Late-joining clients miss prior updates unless the hub is configured for history and the client sends Last-Event-ID. Design the UI to fetch current state over HTTP on load and treat SSE as an invalidation hint.
  • Debugging ladder: curl -N -H 'Accept: text/event-stream' '<public_url>?topic=…' → 401 means token/cookie, 200 with no events means topic mismatch, connection refused means public URL/DNS, works in curl but not the browser means CORS or withCredentials.
  • Extension points: bind your own JWT provider or hub client in the container if you need custom claims (e.g. per-tenant audiences), and keep the topic-derivation logic behind a helper so a future refactor of channel naming doesn't require touching every broadcastOn().
  • Release cadence is modest — pin a known-good version, and when a compatibility release like 3.9.0 lands, bump it alongside your Laravel minor rather than in isolation so constraint conflicts surface together.
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.
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support