はじめに
こんにちは、ひるげです。
最近、個人開発アプリでWeb Push通知を実装しました。
Web Push通知というのは以下のような感じで、ブラウザからプッシュ通知が届くやつです。
この記事では、Web Push通知がどうやって実現されているのかを、原理レベルで解説します。
これを使えば、Webアプリでもネイティブアプリのようなプッシュ通知を送信できるようになります。
メール送信以外のクールな通知機能をWebアプリにつけたい方、ぜひWeb Push通知を自分の手札に加えましょう。
※ ちなみに、スマホでWeb Push通知を受け取らせたい場合は先にPWA対応をする必要があります。PWAについての記事は後日書く予定ですが、すでにインターネット上に色々転がっているので、よければ調べてみて下さい。
2026-04-09追記:PWA記事書きました!これを先に読んでおくと理解がスムーズかもしれません!
Web Pushの登場人物
Web Pushには3人の登場人物がいます。
アプリサーバー
通知を送り出す側。自分が開発したWebアプリのバックエンドです。
「この端末に通知を送りたい」と判断し、プッシュサービスにリクエストを投げます。
プッシュサービス
中継役。ブラウザベンダーが運営しており、ChromeならFCM(Firebase Cloud Messaging)、FirefoxならMozilla Push Serviceが使われています。アプリサーバーから通知を受け取り、ブラウザへ届けます。
ブラウザ
通知を受け取る側。ページを閉じていても、バックグラウンドで動くService Workerがプッシュサービスからのメッセージを待ち受けています。
アプリサーバー → プッシュサービス → ブラウザ
Web Pushの全体フロー
もう少し詳しく見てみましょう。Web Push通知には「購読」と「送信」の2フェーズがあります。
なお、以下ではendpoint・p256dh・auth・VAPID という用語が出てきますが一旦雰囲気で読み進めてください(詳細は後述します)。
endpointは通知送信の宛先、それ以外は暗号化のためのものという認識でいてくれればいいです。
購読フェーズ(ユーザーが通知を許可するとき)
ユーザーがWebアプリ内の「通知を有効にする」ボタン(下画像のようなやつ)をクリックすると、以下の処理が走ります。
- ブラウザがアプリサーバーにVAPID公開鍵を取得しに行く
- ブラウザの通知許可ダイアログが表示される。ユーザーが「許可」をクリックする
- ブラウザ自身が暗号化キー(
p256dh)と認証シークレット(auth)を生成し、1で取得したVAPID公開鍵を含めてプッシュサービスに購読リクエストを送る - プッシュサービスからブラウザへ
endpoint(このユーザー宛に通知を送るためのURL)が返ってくる - ブラウザがこれら(endpoint + p256dh + auth)をアプリサーバーに送って保存してもらう
送信フェーズ(通知を届けるとき)
- アプリサーバーが、プッシュサービスに「この
endpointに通知送信してください」というHTTPリクエストをVAPID秘密鍵の署名付きで送る - プッシュサービスはVAPID公開鍵で署名を検証し、送信元が正しいアプリサーバーであることを確かめられたら、対象のブラウザ(
endpoint)へ届ける - ブラウザのService Workerがメッセージを受け取り、通知を表示する
Service Workerとは?
ページを開いていなくてもバックグラウンドで動き続けるスクリプトのことです。「アプリを開いていないのに通知が表示される」のは、このService Workerがプッシュサービスからのメッセージを常に待ち受けているからです。
フロー中に登場した用語と鍵の役割
さきほど出てきた endpoint・p256dh・auth・VAPID という用語について、概要と役割を述べます。
endpoint
プッシュサービスがユーザーごとに発行するURL。「この人への通知はここに送ってね」という宛先になります。
p256dh(公開鍵)と auth(認証シークレット)
ブラウザが自身で生成します。プッシュサービスは中継役とはいえ通知の内容を見られる立場にあるため、アプリサーバーはこれらを使ってメッセージを暗号化して送ります。p256dh で暗号化されたメッセージは、対応する秘密鍵を持つブラウザだけが復号できます。auth はその暗号をさらに強固にするための乱数シークレットです。
VAPID(Voluntary Application Server Identification)
アプリサーバーが生成する鍵ペア。endpoint のURLさえ知っていれば誰でも通知を送れてしまう問題を防ぐため、サーバーは秘密鍵で署名を付けてリクエストを送ります。プッシュサービスが公開鍵で署名を検証し、「正規のサーバーからのリクエストだ」と確認してから通知を転送します(RFC 8292)。
まとめると以下の表のようになります。
これらを踏まえた上で、もう一度先ほどの全体フローを読み直すとより理解が深まるでしょう。
| 鍵 | 役割 |
|---|---|
| p256dh + auth(ECDH) | メッセージの内容をプッシュサービスから隠す |
| VAPIDキーペア | 送信者が正規のサーバーであることを証明する |
コード例
では実際のコードを見てみましょう。ここではバックエンドをGoで書きます。
以下のコードは仕組みの説明に特化した簡略版です。
実際のプロダクションコードではエラーハンドリングや通知権限のチェックなどを適宜加えてください。
Go以外の言語を使っている方は以下のライブラリが参考になります。
購読フェーズ
フロントエンド側(JavaScript)でService Workerを登録し、プッシュサービスへ購読リクエストを送ります。
(MDN - PushManager.subscribe() をもとにしたサンプルです)
// Service WorkerをページロードなどのタイミングでReady状態にしておく(既に登録済みの場合はそのまま返る)
navigator.serviceWorker.register('/sw.js');
async function subscribePush() {
const registration = await navigator.serviceWorker.ready;
// VAPID公開鍵を取得
// (バックエンドで /api/vapid-public-key というVAPID公開鍵取得用エンドポイントを用意している想定)
const { VAPIDPublicKey } = await fetch('/api/vapid-public-key').then(r => r.json());
// プッシュサービスへ購読リクエストを送る
// urlBase64ToUint8Array: VAPID公開鍵(Base64文字列)をUint8Arrayに変換するヘルパー関数
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPIDPublicKey),
});
// 購読情報(endpoint / p256dh / auth)をアプリサーバーへ送信・保存してもらう
// (バックエンドで /api/subscriptions という、購読情報をDB保存するエンドポイントを用意している想定)
await fetch('/api/subscriptions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription),
});
}
// アプリ内の「通知を許可する」ボタンなどから呼び出す
document.getElementById('enable-push').addEventListener('click', subscribePush);
バックエンド(Go)側では受け取った購読情報をDBに保存しておきます。
type Subscription struct {
Endpoint string `json:"endpoint"`
Keys struct {
P256dh string `json:"p256dh"`
Auth string `json:"auth"`
} `json:"keys"`
}
// リクエストボディをこの構造体にデシリアライズしてDBに保存する
var sub Subscription
json.Unmarshal(reqBody, &sub)
db.Create(&sub)
送信フェーズ
通知を送る際は SherClockHolmes/webpush-go を使います。
以下は同ライブラリのREADMEをもとにしたサンプルです。
import (
"encoding/json"
webpush "github.com/SherClockHolmes/webpush-go"
)
func main() {
// DBから取得した購読情報のJSONをデコード
s := &webpush.Subscription{}
json.Unmarshal([]byte("<DBから取得した購読情報のJSON>"), s)
// s.Endpoint / s.Keys.P256dh / s.Keys.Auth が購読フェーズで保存した値に対応する
// 通知を送信
resp, err := webpush.SendNotification(
[]byte(`{"title":"通知だよ!"}`),
s,
&webpush.Options{
Subscriber: "your@email.com", // 通知送信元(アプリ運営者)のメールアドレス
VAPIDPublicKey: "<YOUR_VAPID_PUBLIC_KEY>",
VAPIDPrivateKey: "<YOUR_VAPID_PRIVATE_KEY>",
TTL: 30, // 秒
},
)
if err != nil {
// エラー処理
}
defer resp.Body.Close()
}
ライブラリが p256dh/auth を使ったメッセージ暗号化と VAPID署名の処理をまるっと引き受けてくれます。
全体フローで示したweb-pushの仕組みは三者が絡み合って結構複雑なのにコードはこれだけ、というのが面白いところです。
先人に感謝ですね。
おわりに
Web Pushは、中継役(プッシュサービス)・内容の暗号化(ECDH)・送信者の認証(VAPID)の3つが組み合わさって実現されています。普段何気なく受け取っている通知の裏側に、こういった仕組みが動いているんですね。
より詳しい仕様は以下を参照してください。
- Push API - MDN Web Docs
- RFC 8292 - Voluntary Application Server Identification (VAPID)
- RFC 8291 - Message Encryption for Web Push
宣伝
最後に少しだけ宣伝をさせてください。
現在、RoambleというiOSアプリを開発しています。
「気になる店があるのに一人で入る勇気が出ない」「新しいお店を開拓したい」そんなあなたの背中を押し、一歩踏み出す体験を経験値に変えるアプリです。
起動すると現在地周辺のスポットが提案され、実際に訪問すると経験値が貯まる...そういったゲーム的な体験を通じて、新たな場所への一歩をサポートします!
現在はWebベータ版を運営中で、iOSウェイトリストを受け付けています。少しでも気になった方は以下のリンクからぜひ覗いてみてください!

