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

# Mercureブロードキャストドライバーの内部構造

> Laravel framework に追加された Mercure ブロードキャストドライバーの仕組みを、ソースコードを元に解説します。Reverb や Pusher とは異なる HTTP/SSEベースのアーキテクチャ、認可Cookie、E2E暗号化チャンネルまで扱います。

<Warning>
  このページで解説する `MercureBroadcaster` は [laravel/framework の PR #61474](https://github.com/laravel/framework/pull/61474)（2026年9月10日マージ）で追加された機能です。執筆時点ではタグ付きリリースにまだ含まれておらず、[公式ドキュメント](https://laravel.com/docs/broadcasting)にも記載がありません。ソースコードとdocblockを元にした先行解説なので、実際に使う場合は`laravel/framework`の変更履歴を確認してください。
</Warning>

## 概要

Laravelのブロードキャストは長らく [Reverb](/jp/broadcasting)・Pusher・Ably という「WebSocketネイティブ」なドライバーを提供してきましたが、`config/broadcasting.php` の`driver`に新しく`mercure`が加わりました。

[Mercure](https://mercure.rocks) は Server-Sent Events (SSE) をベースにしたリアルタイム通信プロトコルです。WebSocketのような双方向コネクションではなく、HTTP/1.1・HTTP/2上のロングポーリングに近い仕組みで動作するため、以下のような特徴があります。

* 通常のHTTPインフラ（リバースプロキシ、CDN、ロードバランサー）を素通りできる
* ブラウザの`EventSource` APIだけでクライアントを実装でき、専用クライアントライブラリなしで動く
* [FrankenPHP](/jp/blog/laravel-cloud) には組み込みのMercureハブが搭載されているため、追加のインフラなしで動かせる

```mermaid theme={null}
flowchart LR
    A["サーバー<br>broadcast(event)"] --> B["MercureBroadcaster"]
    B --> C["Mercureハブ<br>(Hub / FrankenPhpHub)"]
    C -->|"SSE (EventSource)"| D["ブラウザ<br>Laravel Echo"]
    D --> E["UIの<br>リアルタイム更新"]
```

## `driver`の追加とサポートされる値

`config/broadcasting.php` の冒頭コメントに追記されたサポートドライバー一覧に`mercure`が加わりました。

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

接続設定のサンプルも用意されています。

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

`url`を省略した場合、FrankenPHPの組み込みMercureハブ（`mercure_publish()`関数）へ自動フォールバックします。この判定は`CreatesMercureDrivers::mercure()`で行われています。

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

    // ...
}
```

外部にMercureハブを立てている場合は`url`に管理API（publish用）のURL、`public_url`にブラウザから見えるURLを指定します。両者が異なるのは、Docker Composeなど内部ネットワーク経由でpublishし、公開URLだけをブラウザに晒す構成を想定しているためです。

## トークン発行の仕組み：publish用とsubscribe用を分離

MercureはJWT (JSON Web Token) でアクセス制御を行うプロトコルです。`CreatesMercureDrivers`トレイトは、publish（サーバー→ハブ）とsubscribe（ブラウザ→ハブ）で **別々のトークンファクトリー** を組み立てます。

* `secret` / `publish_secret` / `subscribe_secret` を個別に指定でき、省略時は`secret`にフォールバック
* `algorithm` / `publish_algorithm` / `subscribe_algorithm` も同様に個別指定可能（デフォルト`HS256`）
* HS256は32バイト、HS384は48バイト、HS512は64バイト以上のシークレット長が必須で、満たさない場合は`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;
}
```

publishトークンは`Grant::ACTION_PUBLISH`で全トピック（`*`）への発行権限を持ち、`CachingTokenProvider`でメモリキャッシュされます。毎回のブロードキャストごとにJWTを再生成しないための最適化です。

## チャンネル認可とCookieベースの購読モデル

WebSocketドライバーとの最大の違いは、購読側の認可が **単一のCookie** で行われる点です。`MercureBroadcaster::auth()`は`channel_names`配列（1リクエストあたり最大100件）を受け取り、チャンネルごとに認可を判定してから、まとめて1つの認可Cookieを発行します。

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

    // チャンネルごとに認可を判定し Grant を積み上げる...

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

ポイントは「1チャンネルの認可が失敗しても、レスポンス全体を失敗させない」設計です。拒否されたチャンネルは`denied: true`としてレスポンスに含まれるだけで、他の許可されたチャンネルの購読は継続します。これは、Mercureが1つのEventSourceで複数トピックを多重化するため、途中でチャンネルを追加・削除しても既存の接続を切断せずに済むようにするためです。

Presenceチャンネルは通常の`private`/`private-encrypted`チャンネルと異なる`Grant`を発行し、購読URLパターン（Mercureの[Subscription API](https://mercure.rocks/docs/hub/concepts/active-subscriptions)）に対する`subscribe`権限として組み込まれます。

## エンドツーエンド暗号化チャンネル

`private-encrypted-`で始まるチャンネル名は、Mercureハブ自体にもペイロードを見せない「エンドツーエンド暗号化（E2EE）」チャンネルとして扱われます。これは既存のPusher/Reverbドライバーにはない機能です。

`encryption_key`をbase64エンコードされた32バイトの鍵として設定すると`ChannelEncrypter`が有効になり、`broadcast()`実行時にイベント名・ペイロード・ソケットIDをまとめてJWE（JSON Web Encryption）として暗号化します。ハブが中継するのは暗号化済みのバイト列だけです。

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

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

購読側は`auth()`レスポンスに含まれる`jwk`（JSON Web Key）フィールドを使ってブラウザ側で復号します。鍵交換はMercure仕様が推奨する「認可レスポンスに載せてアウトオブバンドで渡す」方式で、ハブ自体は鍵を一切知りません。

<Info>
  Presenceチャンネルは暗号化の対象外です。メンバー一覧がMercureのSubscription APIを経由してハブに見える仕様のため、E2EEとは両立しません。
</Info>

## Whisper（クライアント間の直接メッセージ）

`client_events`が有効な場合（デフォルト`true`）、各ガード付きチャンネルには専用の「whisperトピック」が割り当てられ、購読者はそのトピックに対してのみ`publish`権限を持ちます。

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

チャンネル本体のトピックはサーバー専用のまま保たれるため、クライアントが偽装したサーバーイベントを発行することはできません。Pusherの「クライアントイベント」機能に近い設計ですが、トピックを分離することでより明確に権限を分けています。

## トピックの命名規則

Mercureはハブを複数のアプリケーションで共有できる設計になっており、トピックの衝突を避けるために`topic_prefix`という名前空間を導入しています。デフォルト値は次のとおりです。

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

`.alt`はDNS上で予約された未解決ドメイン（[RFC 9476](https://www.rfc-editor.org/rfc/rfc9476.html)）なので、実在のドメインと衝突する心配がありません。チャンネル名は[RFC 3986](https://www.rfc-editor.org/rfc/rfc3986.html)に従ってURLエンコードされ、単一のパスセグメントに収められます。

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

## Cookieドメインとエラーメッセージ

Mercureハブがアプリケーションと異なるサブドメインで動く構成もよくあるため（例：`mercure.example.com` と `app.example.com`）、認可Cookieのドメイン解決に失敗すると、原因を特定しやすい例外メッセージが投げられます。

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

`__Secure-`や`__Host-`プレフィックス付きのCookie名を使う場合、`public_url`がHTTPSであることも起動時にチェックされます。

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

## Reverb・Pusher・Ablyとの使い分け

| ドライバー         | トランスポート    | インフラ                     | E2E暗号化                   |
| ------------- | ---------- | ------------------------ | ------------------------ |
| Reverb        | WebSocket  | 自前サーバー必須                 | なし                       |
| Pusher / Ably | WebSocket  | SaaS                     | なし                       |
| Mercure       | SSE (HTTP) | 自前ハブ、または FrankenPHP 組み込み | あり（`private-encrypted-`） |

MercureはWebSocketの常時双方向接続が不要なユースケース（通知、進捗表示、チャットのようにサーバー→クライアントの一方向配信が中心の用途）に向いています。FrankenPHPを使っている場合は追加インフラなしで動作するのも利点です。

## 関連ページ

* [ブロードキャストの基礎](/jp/broadcasting)
* [Laravel Cloud](/jp/blog/laravel-cloud)（FrankenPHPベースの実行環境）


## Related topics

- [ブロードキャスト](/jp/broadcasting.md)
- [Redis](/jp/redis.md)
- [Laravel AI SDK 用の Amazon Bedrock ドライバー](/jp/packages/laravel-amazon-bedrock.md)
- [Laravel 11以降の新アプリ構造 FAQ](/jp/advanced/app-structure-faq.md)
- [Illuminate\Support\Manager — ドライバーシステムの解剖](/jp/advanced/manager.md)
