Web Pushを実装する前に知っておきたい仕組みの全体像

Web Pushを実装する前に知っておきたい仕組みの全体像

Web Pushでページを閉じても狙ったブラウザに通知が届く仕組みを、endpoint・VAPID署名・本文暗号化・購読と送信の流れから解説し、React SPAとWeb APIのどこに何を実装するかを整理します。
2026.11.09

Webアプリに通知機能を付けようと思ってWeb Pushを調べ始めると、Service Worker、endpoint、VAPID、p256dhといった用語が一度に出てきます。ライブラリのサンプルをなぞれば動きはしますが、それぞれが何のためにあり、自分はどこに何を実装すればよいのかが腹落ちしないまま進めることになりがちです。

この記事は、Webアプリの開発経験はあるがWeb Pushは初めてというエンジニアに向けて、実装に入る前に知っておきたい仕組みの全体像をまとめたものです。登場人物と大まかな流れから始めて、署名と暗号化、購読と送信の流れを順に見たあと、React SPAとWeb APIの構成に当てはめて、どこに何を書くかを整理します。対象はService Workerを使う従来型のWeb Pushです。

仕様と公式資料は2026年11月9日時点の内容で調べています。確認できた範囲で書いており、確認できなかった点はその場で明記しています。

1. ざっくり全体像

Web Pushとは

Web Pushは、Webサイトがサーバー側からユーザーのブラウザにメッセージを送り、ブラウザがそれをOSの通知として表示する仕組みです。Macなら次の画像のような画面右上に出るバナー、iPhoneならロック画面や通知センターに出る、ネイティブアプリと同じ見た目の通知です。

MacのChromeに表示されたWeb Push通知

表示しているのはページではなくブラウザやOSなので、そのサイトのタブを開いていなくても出ます。

なぜ狙ったブラウザに届くのか

あなたのサーバー、Push配信サービス、ユーザーのブラウザの3つが協調して通知が届く、というところまでは想像がつくと思います。分かりにくいのはその先で、世界中にあるブラウザの中から、配信サービスはどうやって特定のブラウザを狙って届けているのかです。

答えは、ブラウザが先に配信サービスへ「購読」しておき、その結果として受け取る endpoint にあります。

  1. ユーザーが通知を許可すると、ブラウザは配信サービスに対して「このサイトの通知を受け取る」という取り決めを結びます。これを 購読(subscription) と呼びます
  2. 配信サービスは、この購読専用のURLである endpoint を発行してブラウザに返します。配信サービスは、このendpointがどのブラウザのものかを自分の中で覚えています
  3. ブラウザはendpointをあなたのサーバーに渡します。あなたのサーバーは、そのときログインしていたユーザーとendpointを紐づけて保存します。endpoint自体にユーザーの情報は含まれていないので、この紐づけはあなたのサーバーの仕事です
  4. 通知を送りたくなったら、あなたのサーバーは対象ユーザーのendpointを取り出し、そのURLに通知をHTTPで送ります
  5. 配信サービスは、endpointから対応するブラウザを特定し、ブラウザとの間に維持している接続で届けます

つまり「どのブラウザに届けるか」は、あなたのサーバーがどのendpointに送るかで決まり、「endpointがどのブラウザか」は配信サービスが知っている、という分担です。あなたのサーバーが持つのは「ユーザー → endpoint」の対応表、配信サービスが持つのは「endpoint → ブラウザ」の対応です。

ブラウザと配信サービスの接続はページとは無関係に維持されているので、ページを閉じていても⑤は届きます。ポーリングやWebSocketは「あなたのサーバーとページの間」の通信なので、ページを閉じれば終わる点が違います。

なお③でブラウザは、endpointと一緒に本文を暗号化するための鍵も渡しています。鍵の話は3章で扱います。

この仕組みは2つの仕様に分かれて決められています。ブラウザがページに提供するAPI(①〜③の subscribe() や購読情報の形)はW3CのPush API、あなたのサーバーが配信サービスへ送るHTTP(④の送り方)はIETFのRFC 8030です。配信サービスからブラウザへの経路(⑤)はどちらの仕様も縛っておらず、ブラウザベンダーごとの実装です。以降の章では、この流れを順に詳しく見ていきます。

2. ブラウザの中を開く

1章の図の「ユーザーのブラウザ」の中は、実は3つに分かれています。ブラウザ本体が持つPush受信機構と、そのサイトのページで動くJavaScript、そしてService Workerです。1章と同じ配置のまま、ブラウザの箱を開いて描き直します。

色つきの3つが、あなたのWebアプリとして書くコードです。それ以外はブラウザ本体とブラウザベンダーのサービスで、自分では手を入れません。

  • ページのJavaScript。ユーザーの操作をきっかけに、ブラウザの subscribe() を呼んで購読を始めます。返ってきたendpointをあなたのサーバーに送るのもこのコードです。ブラウザと配信サービスの通信には関わりません
  • Push受信機構(ブラウザ本体)。配信サービスと実際にやり取りするのはここです。購読を作り、配信サービスとの接続を維持し、届いたメッセージを復号してService Workerに渡します。ページを閉じても動き続けます
  • Service Worker。届いたメッセージを受けて通知を表示し、タップされたらページを開きます。配信サービスとの接続を持っているのはこちらではなく、Push受信機構です
  • あなたのサーバー。endpointをユーザーと紐づけて保存し、通知を送るときに配信サービスへHTTPリクエストを出します
  • Push配信サービス。endpointを発行し、あなたのサーバーから届いたメッセージを、endpointに対応するブラウザへ配送します

「Push受信機構」をブラウザ本体と書きましたが、環境によっては担当がOSに寄ります。MacのChromeではChrome自身が配信サービスと接続します。iPhoneではネイティブアプリと同じAPNsの接続をOSが維持し、Safari側がWeb Pushの購読に対応づけます。iPhoneで使うときの注意は7章で扱います。

補足:Service Workerとは

Service Workerは、ページとは別にブラウザが動かすJavaScriptです。普段のJavaScriptはタブを閉じれば終わりますが、Service Workerはサイト(オリジン)に対して登録しておくもので、タブが1つも開いていなくても、ブラウザがイベントを渡すときに起動して処理をし、終わればまた止まります。DOMには触れず、ページとはメッセージで通信します。もともとはオフライン対応やキャッシュ制御のために作られた仕組みで、Web Pushはその「ページがなくてもコードを動かせる」性質を借りています。

Web Pushに必要な理由は単純で、ページを閉じている間に通知が届いたとき、通知を表示したりタップ時に開くURLを決めたりするコードが、どこかで動かなければならないからです。Push受信機構が配信サービスからメッセージを受け取ると、そのサイトのService Workerを起動して push イベントを渡します。Service Workerはそこで通知を表示し、ユーザーが通知をタップすると notificationclick イベントで呼ばれてページを開きます。

3. 通知には署名と暗号化がかかっている

購読と送信を詳しく見る前に、全体にかかっている仕組みを押さえておきます。1章の図は、あなたのサーバーが配信サービスに通知を送り、配信サービスがブラウザに届けるだけでした。これだけだと困ることが2つあります。

  • endpointを知っている人なら、誰でもそのブラウザに通知を送れてしまう
  • 配信サービスが、通知の本文をすべて読めてしまう

Web Pushは、前者をVAPIDという署名の仕組みで、後者をMessage Encryptionという暗号化の仕組みで防いでいます。それぞれ別のRFCで決められていて、使う鍵も別です。

仕組み 防ぐこと 鍵を作るのは 鍵で何をするか
VAPID(RFC 8292) あなた以外が通知を送ること あなた(全ユーザー共通で1組) あなたのサーバーが署名し、配信サービスが確かめる
Message Encryption(RFC 8291) 配信サービスに本文を読まれること ブラウザ(購読ごと) あなたのサーバーが暗号化し、ブラウザが復号する

どちらの仕組みも、鍵の受け渡しは購読のときに済ませ、送信のときにその鍵を使います。3.1節と3.2節でそれぞれの鍵がどこを通って渡るかを見たあと、3.3節で実際に送るHTTPリクエストのどこに署名と暗号化がかかっているかを確かめます。

3.1 VAPID:送ってきたのが誰かを確かめる

デプロイ前に、あなたがVAPID鍵ペアを1組作ります。秘密鍵はあなたのサーバーだけに置き、公開鍵はページのJavaScriptに書いておきます。

  1. 購読時。ページのJavaScriptが subscribe() に公開鍵を渡し、ブラウザがそれを配信サービスに届けます。配信サービスは、発行するendpointとこの公開鍵を結びつけて覚えます
  2. 送信時。あなたのサーバーは、秘密鍵で作った署名と公開鍵を付けてendpointに送ります。配信サービスは、公開鍵が購読時に登録されたものと同じか、署名が正しいかを確かめます

公開鍵は誰でも知ることができますが、署名を作れるのは秘密鍵を持つあなたのサーバーだけです。そのため、endpointが漏れても、他人がそこに通知を送ることはできません。

ただし、VAPIDが保証するのは「送ってきたのがあなたのサーバーである」ことだけです。そのendpointが本当に通知したいユーザーのものかどうかは配信サービスには分からず、あなたのサーバーが保存している「ユーザー → endpoint」の対応が正しいかどうかにかかっています。

3.2 Message Encryption:本文を読めるのを宛先のブラウザだけにする

こちらは鍵を作るのがブラウザで、VAPIDとは向きが逆です。ブラウザは購読のときに、あなたのサーバーへ「自分宛てのメッセージを暗号化して書く権利」を渡し、読む権利は手元に残します。

  1. 購読時。ブラウザは購読ごとに鍵ペアを作り、公開鍵(p256dh)を、合言葉にあたる auth と一緒にあなたのサーバーへ渡します。秘密鍵はブラウザの外に出ません
  2. 送信時。あなたのサーバーは p256dh と auth で本文を暗号化して送ります。配信サービスは中身を読めないまま運びます
  3. 受信時。ブラウザは自分の秘密鍵と auth で復号します

auth は、ブラウザとあなたのサーバーだけが知る値で、暗号化の鍵を作るときに混ぜます。公開鍵は公開してもよいという前提の値なので、それとは別に秘密にしておく値を用意している、と捉えておけば十分です。

なお、暗号化で隠せるのは本文だけです。「どのサーバーがどのブラウザにいつ送ったか」は配信サービスから見えています。

3.3 送信するリクエストの中身

3.1節のVAPIDと3.2節のMessage Encryptionは、あなたのサーバーがendpointに送る1本のHTTP POSTの中で、別々の場所にかかっています。送信時に出ていくリクエストを見て、ここまでの話を確かめます。

endpointに送るHTTP POSTの中身(値は例)
POST /fcm/send/dU5-cS8TRNY:APA91b... HTTP/1.1
Host: fcm.googleapis.com

# ── VAPID(RFC 8292):配信サービスが検証する。改ざんされれば弾かれる ──
Authorization: vapid t=<JWT:aud・exp・sub をVAPID秘密鍵で署名>, k=<VAPID公開鍵>

# ── 配信の指示(RFC 8030):配信サービスが読む ──
TTL: 86400
Content-Encoding: aes128gcm

# ── Message Encryption(RFC 8291):配信サービスは読めない。宛先のブラウザだけが復号できる ──
<p256dhとauthで暗号化された本文>
  • Authorizationヘッダ:VAPIDで署名されるのは、送り先(aud)・有効期限(exp)・連絡先(sub)を書いたJWTです。配信サービスはこれを検証して送信元を確かめます。通知の本文はこの署名の対象に入っていません
  • body:通知の本文は暗号化してここに入れます。配信サービスは中身を読めず、復号できるのは宛先のブラウザだけです。途中で改ざんされていればブラウザ側の復号が失敗します

署名と暗号化は別の場所にかかる別の操作なので、配信サービスがVAPIDの鍵で本文を復号する、ということは起きません。Node.jsの web-push などのライブラリを使えば、このリクエストの組み立てはライブラリが行います。

4. 購読を詳しく見る

1章の①〜③、ユーザーがボタンを押してから、あなたのサーバーに購読情報が保存されるまでを詳しく見ます。3章で見た鍵の受け渡しも、すべてこの中で行われます。

経路は図のとおりです。実装で押さえておきたいのは次の3点です。

  • Service Workerの登録を先に済ませます。ページを閉じた後にメッセージを受け取るのはService Workerなので、購読はService Workerの登録に結びつけて作られます
  • 購読はユーザーの操作を起点にします。購読には通知の許可が必要です。Safariは、購読の要求がボタンのクリックなどユーザー操作の中で行われることを求めています
  • subscribe() に渡すのは2つです。userVisibleOnly: true は「受信したら必ず通知を表示する」という約束で、Chromeでは必須です。applicationServerKey は3.1節のVAPID公開鍵で、仕様上は省略できますが、Chromeでは実質必須です

ブラウザから返ってくる購読情報(PushSubscription)の中身は次のとおりです。

値 中身 何に使うか
endpoint 配信サービスが発行した送信先URL あなたのサーバーが通知をPOSTする宛先
expirationTime 購読の有効期限。Chromeでは null 期限があれば、それまでに購読し直す
keys.p256dh ブラウザが作った鍵ペアの公開鍵 本文の暗号化。3.2節で登場
keys.auth ブラウザが作った16バイトの合言葉 同上

ここで大事なのは、購読情報にはユーザーが誰かという情報が入っていないことです。入っているのは送り先と暗号化の情報だけです。Push APIは、endpointから配信サービス以外の誰かがユーザーの情報を引き出せてはいけない、と決めています。だから、ログインセッションから分かるユーザーと購読を結びつけて保存するのは、あなたのサーバーの仕事です。

4.1 endpoint:購読ごとに発行される宛先

endpointは、全ブラウザ共通の送信先ではありません。購読したブラウザの配信サービスが、購読1件ごとに発行するURLです。Chromeで購読すると、次のような形になります。

https://fcm.googleapis.com/fcm/send/dU5-cS8TRNY:APA91bFPsN-cAs8L...(長いランダム文字列)

ホストの部分は、どの配信サービスが発行したかで決まります。Chromeなら fcm.googleapis.com、iPhoneなら *.push.apple.com 配下です。後ろの長いランダム文字列は、配信サービスが「どの購読宛てか」を見分けるためのトークンの役割をしています。あなたのサーバーがこの中身を読み解く必要はなく、保存したURLにそのままPOSTすれば、そのURLを発行した配信サービスが該当するブラウザへ届けます。

トークンなので、endpointは購読ごとに別の値になります。

  • 同じユーザーでも、MacのChromeとiPhoneで購読すれば、別々のendpointが2つできます
  • 同じブラウザでも、購読し直せば新しいendpointに変わります

そのため、あなたのサーバーはユーザー1人に対してendpointを複数持てるように保存し、購読し直されたら新しいendpointに更新します。

5. 送信から表示までを詳しく見る

1章の④⑤、あなたのサーバーが「このユーザーに通知を送りたい」と思ってから、ユーザーが通知をタップするまでです。

経路は図のとおりです。実装で押さえておきたいのは次の4点です。

  • 201は「預かりました」であって「届けました」ではありません。配信サービスがメッセージを預かっておく最大時間を、TTLヘッダで指定します。ブラウザがオフラインなら、その間預かってつながったときに届けます
  • 届くことも、すぐ届くことも保証されていません。配信サービスからブラウザへの経路は、RFC 8030にHTTP/2を使う方法が一例として書かれているものの、ブラウザがそれに従う義務はなく、実際にはChromeはFCM、SafariはAPNsというベンダー独自の経路を使っています
  • 購読は失効することがあります。失効した購読に送ると404や410が返るので、あなたのサーバーはその購読を削除します
  • pushイベントでは必ず通知を表示します。userVisibleOnly: true で購読しているためです。Safariは、受信後すぐに通知を表示しないサイトの通知許可を取り消します

event.data には、ブラウザが復号した後の平文が入っています。Service Workerで復号処理を書く必要はありません。

6. React SPAとWeb APIに当てはめる

ここまでの仕組みを、よくある構成に当てはめてみます。例として、ReactのSPAと、別に動くWeb APIという構成を考えます。

置き場所 書くこと 関係する章
Reactコンポーネント ボタンのクリックでService Workerを登録し、VAPID公開鍵を渡して subscribe() を呼ぶ。返ってきた購読情報をAPIに送る 4章
public/sw.js push イベントで通知を表示し、notificationclick イベントでページを開く 5章
POST /api/push-subscriptions ログイン中のユーザーと購読情報をテーブルに保存する 4章
通知の送信処理 「コメントが付いた」など通知したい処理から呼ぶ。テーブルから対象ユーザーの購読情報を取り出し、暗号化と署名をしてendpointにPOSTする。404や410が返った行は削除する 3章、5章
設定 VAPID公開鍵はフロントエンドの環境変数に、秘密鍵はWeb APIのシークレットに置く 3.1節

構成上のポイントは3つです。

  • Service WorkerはReactのバンドルに含めず、別ファイルにします。Service Workerはページとは別に動くスクリプトで、登録したファイルの場所によって対象範囲が決まります。public/sw.js に置いてサイトのルートから配信すると、サイト全体が対象になります
  • テーブルはユーザー1人に複数行を持てるようにします。4.1節のとおり、ブラウザごと、購読し直すごとにendpointが変わるためです
  • 送信処理の暗号化と署名は、ライブラリに任せます。Node.jsなら web-push などが処理します。MacのChromeなら、VAPIDを使う限りGoogle側でアカウントやプロジェクトを作る作業もありません

通知の本文には「新着があります」程度の情報だけを載せ、詳しい内容は通知から開いたページがAPIから取得する、という設計にしておくと、本文の取り扱いに悩まずに済みます。ページ側ではいつもどおりログイン状態と閲覧権限を確認できます。

7. iPhoneで使うときの注意

ここまでの仕組みは、iPhoneでも同じ標準に沿っています。配信サービスはAppleのもの(endpointは *.push.apple.com 配下)になりますが、受け付けるのは同じWeb Pushの規格なので、あなたのサーバーのコードは変わりません。ただし、使い始めるまでの条件に注意が要ります。

ホーム画面に追加したWebアプリでしか使えません。iPhoneでWeb Pushが使えるのは、Safariで「ホーム画面に追加」したWebアプリだけで、iOS 16.4以降が必要です。通知の許可も、そのアプリ内のボタンのクリックなどユーザー操作を起点に求めます。通常のSafariのタブでは購読できないので、iPhone向けには「ホーム画面に追加してもらう」案内が必要になります。

試すにはHTTPSが必要です。Macのブラウザは http://localhost を例外的に安全なオリジンとして扱うので、開発中はHTTPのままService WorkerもPushも試せます。一方、iPhoneからMacに http://192.168.x.x:3000 のようにLAN内のIPアドレスでつないでも、それはlocalhostではないので安全なオリジンになりません。iPhoneで試すには、自己署名証明書を信頼させるか、トンネルサービスなどで正規の証明書付きのHTTPSを用意します。

補足:ネイティブアプリのAPNsとの違い

iPhoneのネイティブアプリに通知を送るときもAPNsを使いますが、こちらはAppleが発行する認証用の鍵を使い、APNs独自のAPIに送る、まったく別の方式です。Web Pushでも端末までの配送にはAppleのAPNsの仕組みが使われますが、あなたのサーバーから見える入口はWeb Pushの規格に沿ったendpointです。名前が同じなので混同しやすいのですが、Web Pushを実装するときにネイティブアプリ向けのAPNsの資料を読む必要はありません。

補足:Declarative Web Push

従来方式のService Workerは、pushイベントで通知を表示し、タップでURLを開くだけの定型的なコードです。それでも自分のオリジンに置いて自分で書く必要があります。この「定型なのに毎回自前で書く」状態への回答として、WebKitはService Workerなしで購読と通知表示ができるDeclarative Web Pushを、iOS 18.4、iPadOS 18.4、Safari 18.4で導入しています。ペイロードを決められたJSON形式にすると、ブラウザが直接通知を表示します。WebKitは、この形式のメッセージは非対応ブラウザでは従来のService Worker方式で処理されると説明しています。この記事は従来方式を対象にしているため、「すべてのWeb PushにService Workerが必須」とは言えない点だけ補足しておきます。

まとめ

  • ページを閉じても届くのは、ブラウザ本体(iPhoneではOS)が配信サービスとの接続を維持し、届いたメッセージでService Workerを起動して通知を出すからです
  • 通知にはVAPIDの署名とMessage Encryptionの暗号化がかかっています。endpointが漏れても他人は送れず、配信サービスは本文を読めません。どちらの鍵も購読のときに受け渡します
  • endpointは配信サービスが購読1件ごとに発行する送信先URLで、ブラウザごと、購読し直すごとに変わります。ユーザーと購読を結びつけているのは、あなたのサーバーだけです
  • 自分で書くのは、購読を作るフロントエンド、通知を出すService Worker、購読を保存して送信するAPIの3つです。送信のコードは配信サービスによらず共通で、iPhoneではホーム画面に追加したWebアプリが前提になります

おわりに

Web Pushを仕様に沿って追ってみると、Web Pushの規格で決まっている範囲と、ブラウザベンダーが独自に作っている範囲の境界がはっきり見えました。あなたのサーバーから配信サービスへの送信、署名、暗号化は規格で統一されていて、配信サービスからブラウザまでの配送はベンダーごとの実装です。この境界が見えると、どこまでが自分のコードの責任で、どこからがブラウザや配信サービスに任せる部分なのかを切り分けて設計できます。

このブログがどなたかのお役に立てれば幸いです。

参考資料

この記事をシェアする

関連記事