> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Onder de motorkap van de Mercure-broadcastdriver

> Een uitleg op broncodeniveau van de Mercure-broadcastdriver die aan het Laravel-framework is toegevoegd: de op HTTP/SSE gebaseerde architectuur, gescheiden publish- en subscribe-JWT's, kanaalautorisatie via één enkele cookie en end-to-end versleutelde kanalen.

<Warning>
  De `MercureBroadcaster` die op deze pagina wordt behandeld, is toegevoegd in [laravel/framework PR #61474](https://github.com/laravel/framework/pull/61474) (gemerged op 10 september 2026). Op het moment van schrijven maakt hij nog geen deel uit van een getagde release en staat hij ook niet in de [officiële documentatie](https://laravel.com/docs/broadcasting). Deze pagina is een preview op basis van de broncode en docblocks — controleer de changelog van `laravel/framework` voordat je hier in productie op vertrouwt.
</Warning>

## Overzicht

De broadcastinglaag van Laravel biedt al lange tijd de "WebSocket-native" drivers [Reverb](/nl/broadcasting), Pusher en Ably. Aan `config/broadcasting.php` is nu een nieuwe driverwaarde `mercure` toegevoegd.

[Mercure](https://mercure.rocks) is een realtime protocol dat op Server-Sent Events (SSE) is gebouwd in plaats van op een bidirectionele WebSocket-verbinding. Het gedraagt zich meer als long-polling over HTTP/1.1 of HTTP/2, wat een aantal onderscheidende eigenschappen oplevert:

* Het gaat transparant door gewone HTTP-infrastructuur heen (reverse proxies, CDN's, load balancers).
* Clients kunnen worden geïmplementeerd met alleen de `EventSource`-API van de browser — er is geen aparte clientbibliotheek nodig.
* [FrankenPHP](/nl/blog/laravel-cloud) heeft een ingebouwde Mercure-hub, dus je hebt geen extra infrastructuur nodig om ermee te experimenteren.

```mermaid theme={null}
flowchart LR
    A["Server<br>broadcast(event)"] --> B["MercureBroadcaster"]
    B --> C["Mercure-hub<br>(Hub / FrankenPhpHub)"]
    C -->|"SSE (EventSource)"| D["Browser<br>Laravel Echo"]
    D --> E["Realtime<br>UI-update"]
```

## De nieuwe `driver`-waarde

In de commentaarregel bovenaan `config/broadcasting.php` met ondersteunde drivers staat nu ook `mercure`:

```php theme={null}
// Supported: "reverb", "pusher", "ably", "mercure", "redis", "log", "null"
```

Er is ook een voorbeeldconfiguratie voor de connectie beschikbaar:

```php theme={null}
'mercure' => [
    'driver' => 'mercure',
    'url' => env('MERCURE_URL'),
    'public_url' => env('MERCURE_PUBLIC_URL'),
    'secret' => env('MERCURE_JWT_SECRET'),
    'encryption_key' => env('MERCURE_ENCRYPTION_KEY'),
    'claims' => [
        'iss' => env('MERCURE_JWT_ISSUER'),
        'client_id' => env('APP_NAME'),
    ],
    'cookie_name' => env('MERCURE_COOKIE_NAME'),
    'subscribe_expiration' => (int) env('MERCURE_SUBSCRIBE_EXPIRATION', 5),
],
```

Als je `url` weglaat, valt de driver terug op de ingebouwde Mercure-hub van FrankenPHP (de functie `mercure_publish()`). Die beslissing wordt genomen in `CreatesMercureDrivers::mercure()`:

```php theme={null}
public function mercure(array $config)
{
    if (empty($config['url'])) {
        return $this->frankenPhpMercure($config);
    }

    // ...
}
```

Als je een externe hub draait, moet `url` verwijzen naar de management-API die voor publishen wordt gebruikt, terwijl `public_url` de URL is waarmee de browser verbinding maakt. Door beide te splitsen worden opstellingen ondersteund waarbij publishen via een intern netwerk gebeurt (bijvoorbeeld Docker Compose) en alleen de publieke URL naar de browser wordt blootgesteld.

## Gescheiden publish- en subscribe-tokens

Mercure is een op JWT gebaseerd protocol voor toegangscontrole. De trait `CreatesMercureDrivers` bouwt **aparte token-factories** voor publishen (server → hub) en subscriben (browser → hub):

* `secret`, `publish_secret` en `subscribe_secret` kunnen onafhankelijk van elkaar worden geconfigureerd; bij afwezigheid vallen ze terug op `secret`.
* `algorithm`, `publish_algorithm` en `subscribe_algorithm` kunnen op dezelfde manier per kant worden ingesteld (standaard `HS256`).
* HS256 vereist een secret van minimaal 32 bytes, HS384 minimaal 48 en HS512 minimaal 64; anders wordt een `InvalidArgumentException` gegooid.

```php theme={null}
protected function mercureSecret(array $config, string $side)
{
    $secret = ($config[$side.'_secret'] ?? null) ?: ($config['secret'] ?? null);

    // ...

    $minimumLength = ['HS256' => 32, 'HS384' => 48, 'HS512' => 64][$algorithm] ?? 0;

    if (strlen($secret) < $minimumLength) {
        throw new InvalidArgumentException(/* ... */);
    }

    return $secret;
}
```

Het publish-token draagt `Grant::ACTION_PUBLISH` voor alle topics (`*`) en wordt gememoïseerd door `CachingTokenProvider`, zodat er niet bij elke afzonderlijke broadcast-aanroep een nieuwe JWT hoeft te worden gegenereerd.

## Kanaalautorisatie via één enkele cookie

Het grootste architecturale verschil met de WebSocket-drivers is dat de autorisatie van abonnees via **één cookie** verloopt. `MercureBroadcaster::auth()` accepteert een array `channel_names` (met een limiet van 100 per request), beoordeelt de autorisatie per kanaal en geeft vervolgens één enkele autorisatiecookie uit die alle kanalen omvat:

```php theme={null}
public function auth($request)
{
    $channelNames = (array) $request->input('channel_names', []);

    if ($channelNames === [] ||
        count($channelNames) > 100 ||
        $channelNames !== array_filter($channelNames, 'is_string')) {
        throw new AccessDeniedHttpException;
    }

    // Evalueer autorisatie per kanaal en verzamel de grants...

    return (new JsonResponse([/* ... */]))
        ->cookie($this->makeAuthorizationCookie($request, $grants, $user));
}
```

Cruciaal is dat één geweigerd kanaal niet de hele respons laat mislukken — het wordt in de responsepayload gemarkeerd als `denied: true`, terwijl de overige geautoriseerde kanalen gewoon blijven werken. Dat is belangrijk omdat Mercure meerdere topics multiplext over één EventSource-verbinding, zodat kanalen halverwege een sessie kunnen worden toegevoegd of verwijderd zonder de verbinding te verbreken.

Presence-kanalen krijgen een andere `Grant`-vorm dan gewone `private`- of `private-encrypted`-kanalen, gericht op het subscribe-URL-patroon dat door Mercure's [Subscription-API](https://mercure.rocks/docs/hub/concepts/active-subscriptions) wordt gebruikt.

## End-to-end versleutelde kanalen

Kanalen met het prefix `private-encrypted-` worden behandeld als end-to-end versleuteld (E2EE) — dat betekent dat zelfs de Mercure-hub zelf de payload nooit te zien krijgt. Dit is een mogelijkheid die de bestaande Pusher- en Reverb-drivers niet bieden.

Als je een base64-gecodeerde `encryption_key` van 32 bytes instelt, wordt de `ChannelEncrypter` geactiveerd. Die verpakt de eventnaam, de payload en het socket-ID in een JWE (JSON Web Encryption) voordat `broadcast()` het verstuurt. De hub geeft alleen versleutelde bytes door.

```php theme={null}
'encryption_key' => env('MERCURE_ENCRYPTION_KEY'),
```

```shell theme={null}
php -r "echo base64_encode(random_bytes(32));"
```

Abonnees ontsleutelen in de browser met behulp van het `jwk`-veld (JSON Web Key) dat door de `auth()`-respons wordt teruggegeven — een out-of-band-sleuteluitwisseling die door de Mercure-specificatie wordt aanbevolen, waardoor de hub de sleutel nooit leert kennen.

<Info>
  Presence-kanalen kunnen niet worden versleuteld, omdat hun ledenlijst per ontwerp via de Subscription-API van de hub loopt — dat is onverenigbaar met E2EE.
</Info>

## Whispers (directe berichten tussen clients)

Wanneer `client_events` is ingeschakeld (de standaard), krijgt elk beschermd kanaal een speciaal "whisper-topic" toegewezen waar abonnees op mogen publishen:

```php theme={null}
if ($this->clientEvents && $whisperTopics !== []) {
    $grants[] = new Grant([Grant::ACTION_SUBSCRIBE, Grant::ACTION_PUBLISH], $whisperTopics);
}
```

Het eigenlijke topic van het kanaal blijft alleen voor de server, zodat een client nooit een event kan vervalsen dat afkomstig lijkt van de server. Dit lijkt op de "client events"-functie van Pusher, maar houdt de autorisatie duidelijk gescheiden door aparte topics te gebruiken.

## Naamgeving van topics

Omdat Mercure-hubs vaak door meerdere applicaties worden gedeeld, worden topics genamespaced onder een `topic_prefix` om botsingen te voorkomen. De standaardwaarde is:

```php theme={null}
protected string $topicPrefix = 'https://laravel.alt/echo/',
```

`.alt` is een gereserveerd, niet-resolvbaar DNS-suffix ([RFC 9476](https://www.rfc-editor.org/rfc/rfc9476.html)), dus het kan nooit botsen met een echt domein. Kanaalnamen worden URL-encoded volgens [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986.html) en in één padsegment gestopt:

```php theme={null}
protected function channelTopic($channelName)
{
    return $this->topicPrefix.'channel/'.rawurlencode($channelName);
}
```

## Cookiedomein en foutmeldingen

Omdat een Mercure-hub vaak op een ander subdomein draait dan de applicatie (bijvoorbeeld `mercure.example.com` versus `app.example.com`), wordt er een beschrijvende exception gegooid als er geen gedeeld cookiedomein kan worden bepaald:

```php theme={null}
throw new BroadcastException(sprintf(
    'Mercure error: %s. Adjust the Mercure "public_url" configuration value so the hub [%s] shares a registrable domain with the application host [%s].',
    rtrim($e->getMessage(), '.'), $this->hub->getPublicUrl(), $request->getHost()
), 0, $e);
```

Als je een cookienaam gebruikt met het prefix `__Secure-` of `__Host-`, controleert de driver bij het opstarten ook of `public_url` HTTPS is:

```php theme={null}
if (str_starts_with($hub->getCookieName(), '__') &&
    parse_url($hub->getPublicUrl(), PHP_URL_SCHEME) === 'http') {
    throw new InvalidArgumentException(/* ... */);
}
```

## Kiezen tussen Reverb, Pusher/Ably en Mercure

| Driver        | Transport  | Infrastructuur                               | E2E-versleuteling         |
| ------------- | ---------- | -------------------------------------------- | ------------------------- |
| Reverb        | WebSocket  | Zelf gehoste server verplicht                | Nee                       |
| Pusher / Ably | WebSocket  | SaaS                                         | Nee                       |
| Mercure       | SSE (HTTP) | Zelf gehoste hub, of ingebouwd in FrankenPHP | Ja (`private-encrypted-`) |

Mercure is een goede keuze wanneer je geen persistente bidirectionele verbinding nodig hebt — denk aan notificaties, voortgangsupdates of chat-achtige use cases die vooral van server naar client gaan. Als je al FrankenPHP draait, werkt het zelfs zonder enige extra infrastructuur.

## Gerelateerde pagina's

* [Basis van broadcasting](/nl/broadcasting)
* [Laravel Cloud](/nl/blog/laravel-cloud) (op FrankenPHP gebaseerde runtime)


## Related topics

- [De interne structuur van package discovery](/nl/advanced/package-discovery.md)
- [Versiecompatibiliteit van packages beheren](/nl/advanced/package-versioning.md)
- [Uitvoeringscontrole van queue-jobs](/nl/advanced/queue-job-control.md)
- [FAQ over de nieuwe appstructuur van Laravel 11+](/nl/advanced/app-structure-faq.md)
- [Custom casts van Eloquent](/nl/advanced/eloquent-casts.md)
