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

# Dentro del driver de broadcasting Mercure

> Un recorrido a nivel de código fuente por el driver de broadcasting Mercure añadido al framework de Laravel: su arquitectura HTTP/SSE, los JWT separados para publicación y suscripción, la autorización de canales mediante una sola cookie y los canales cifrados de extremo a extremo.

<Warning>
  El `MercureBroadcaster` que se explica aquí se añadió en el [PR #61474 de laravel/framework](https://github.com/laravel/framework/pull/61474) (mergeado el 10 de septiembre de 2026). En el momento de escribir esto todavía no forma parte de una release etiquetada y no está documentado en el [sitio de documentación oficial](https://laravel.com/docs/broadcasting). Esta página es una vista previa basada en el código fuente y los docblocks — revisa el changelog de `laravel/framework` antes de depender de él en producción.
</Warning>

## Visión general

La capa de broadcasting de Laravel ha ofrecido durante mucho tiempo [Reverb](/es/broadcasting), Pusher y Ably como drivers «nativos de WebSocket». Ahora se ha añadido un nuevo valor `mercure` para el `driver` en `config/broadcasting.php`.

[Mercure](https://mercure.rocks) es un protocolo en tiempo real construido sobre Server-Sent Events (SSE) en lugar de una conexión WebSocket bidireccional. Se comporta más como long-polling sobre HTTP/1.1 o HTTP/2, lo que le da varias propiedades distintivas:

* Atraviesa la infraestructura HTTP habitual (reverse proxies, CDNs, balanceadores de carga) de forma transparente.
* Los clientes se pueden implementar únicamente con la API `EventSource` del navegador — no hace falta ninguna librería cliente dedicada.
* [FrankenPHP](/es/blog/laravel-cloud) incluye un hub Mercure integrado, así que no necesitas infraestructura adicional para probarlo.

```mermaid theme={null}
flowchart LR
    A["Servidor<br>broadcast(event)"] --> B["MercureBroadcaster"]
    B --> C["Hub de Mercure<br>(Hub / FrankenPhpHub)"]
    C -->|"SSE (EventSource)"| D["Navegador<br>Laravel Echo"]
    D --> E["Actualización<br>en tiempo real de la UI"]
```

## El nuevo valor de `driver`

El comentario con los drivers soportados que aparece al principio de `config/broadcasting.php` ahora lista `mercure`:

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

También se incluye una configuración de conexión de ejemplo:

```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),
],
```

Si omites `url`, el driver hace fallback al hub Mercure integrado de FrankenPHP (la función `mercure_publish()`). Esa decisión se toma en `CreatesMercureDrivers::mercure()`:

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

    // ...
}
```

Cuando ejecutas un hub externo, `url` debe apuntar a la API de administración usada para publicar, mientras que `public_url` es la URL a la que se conecta el navegador. Separar ambos soporta configuraciones donde la publicación ocurre a través de una red interna (por ejemplo, Docker Compose) mientras que solo se expone la URL pública al navegador.

## Tokens separados para publicación y suscripción

Mercure es un protocolo de control de acceso basado en JWT. El trait `CreatesMercureDrivers` construye **fábricas de tokens separadas** para publicar (servidor → hub) y suscribirse (navegador → hub):

* `secret`, `publish_secret` y `subscribe_secret` se pueden configurar de forma independiente, con fallback a `secret`.
* `algorithm`, `publish_algorithm` y `subscribe_algorithm` también se pueden fijar por cada lado (por defecto `HS256`).
* HS256 requiere un secret de al menos 32 bytes, HS384 al menos 48 y HS512 al menos 64; en caso contrario se lanza una `InvalidArgumentException`.

```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;
}
```

El token de publicación lleva `Grant::ACTION_PUBLISH` para todos los topics (`*`) y se memoiza mediante `CachingTokenProvider`, de modo que no se acuñe un JWT nuevo en cada llamada a broadcast.

## Autorización de canales mediante una sola cookie

La mayor diferencia arquitectónica respecto a los drivers WebSocket es que la autorización del suscriptor se gestiona a través de **una única cookie**. `MercureBroadcaster::auth()` acepta un array `channel_names` (con un máximo de 100 por petición), evalúa la autorización por canal y luego emite una sola cookie de autorización que los cubre todos:

```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;
    }

    // Evalúa la autorización por canal y acumula los grants...

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

Lo importante es que un único canal denegado no hace fallar toda la respuesta — se marca como `denied: true` en el payload de la respuesta mientras que el resto de canales autorizados siguen funcionando. Esto importa porque Mercure multiplexa muchos topics sobre una sola conexión EventSource, de forma que se pueden añadir o quitar canales en mitad de la sesión sin necesidad de cortar la conexión.

Los canales de presence reciben una forma de `Grant` distinta a los canales `private`/`private-encrypted` normales, orientada al patrón de URL de suscripción usado por la [Subscription API](https://mercure.rocks/docs/hub/concepts/active-subscriptions) de Mercure.

## Canales con cifrado de extremo a extremo

Los canales cuyo nombre empieza por `private-encrypted-` se tratan como canales cifrados de extremo a extremo (E2EE) — lo que significa que ni siquiera el propio hub de Mercure ve nunca el payload. Esta es una capacidad que los drivers existentes de Pusher y Reverb no ofrecen.

Configurar una `encryption_key` de 32 bytes codificada en base64 activa el `ChannelEncrypter`, que envuelve el nombre del evento, el payload y el socket ID en un JWE (JSON Web Encryption) antes de que `broadcast()` los envíe. El hub solo retransmite bytes cifrados.

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

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

Los suscriptores descifran en el navegador usando el campo `jwk` (JSON Web Key) devuelto por la respuesta de `auth()` — un intercambio de claves fuera de banda recomendado por la especificación de Mercure, de modo que el hub nunca llega a conocer la clave.

<Info>
  Los canales de presence no se pueden cifrar, ya que su lista de miembros circula por la Subscription API del hub por diseño — eso es incompatible con E2EE.
</Info>

## Whispers (mensajes directos entre clientes)

Cuando `client_events` está habilitado (por defecto), cada canal protegido recibe un «whisper topic» dedicado sobre el que los suscriptores pueden publicar:

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

El topic propio del canal se mantiene como server-only, de manera que un cliente nunca puede falsificar un evento originado en el servidor. Esto refleja la funcionalidad de «client events» de Pusher, pero mantiene la autorización claramente separada usando topics distintos.

## Nomenclatura de topics

Como los hubs de Mercure suelen compartirse entre varias aplicaciones, los topics se agrupan bajo un `topic_prefix` para evitar colisiones. El valor por defecto es:

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

`.alt` es un sufijo DNS reservado y no resoluble ([RFC 9476](https://www.rfc-editor.org/rfc/rfc9476.html)), así que nunca puede chocar con un dominio real. Los nombres de canal se codifican como URL según la [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986.html) en un único segmento de ruta:

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

## Dominio de la cookie y mensajes de error

Como un hub de Mercure a menudo se ejecuta en un subdominio distinto al de la aplicación (por ejemplo, `mercure.example.com` frente a `app.example.com`), un fallo al resolver un dominio de cookie compartido lanza una excepción descriptiva:

```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);
```

Si usas un nombre de cookie con prefijo `__Secure-` o `__Host-`, el driver también verifica en el arranque que `public_url` sea HTTPS:

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

## Cuándo elegir Reverb, Pusher/Ably o Mercure

| Driver        | Transporte | Infraestructura                            | Cifrado E2E               |
| ------------- | ---------- | ------------------------------------------ | ------------------------- |
| Reverb        | WebSocket  | Servidor autoalojado obligatorio           | No                        |
| Pusher / Ably | WebSocket  | SaaS                                       | No                        |
| Mercure       | SSE (HTTP) | Hub autoalojado, o integrado en FrankenPHP | Sí (`private-encrypted-`) |

Mercure encaja bien cuando no necesitas una conexión bidireccional persistente — notificaciones, actualizaciones de progreso o casos de uso tipo chat que son principalmente de servidor a cliente. Si ya estás ejecutando FrankenPHP, funciona sin ninguna infraestructura adicional.

## Páginas relacionadas

* [Fundamentos de broadcasting](/es/broadcasting)
* [Laravel Cloud](/es/blog/laravel-cloud) (runtime basado en FrankenPHP)


## Related topics

- [Broadcasting](/es/broadcasting.md)
- [Illuminate\Support\Manager — anatomía del sistema de drivers](/es/advanced/manager.md)
- [Almacenamiento de archivos](/es/filesystem.md)
- [Laravel Sentinel — Investigación del middleware de protección de rutas](/es/blog/sentinel-introduction.md)
- [Concurrencia](/es/concurrency.md)
