この記事は LITALICO Advent Calendar 2025 のシリーズ1の7日目の記事です。
1. はじめに
こんにちは、エンジニアリング本部 プロダクトエンジニアリング(PE)部の かたぎり です。
今回は、PE部のプロダクトで、ブラウザへのプッシュ通知を実装するかもしれない...という話が出たため、その技術検証(スパイク)としてブラウザ通知(ウェブプッシュ)を実装してみた際のログを共有します。
今回のスコープ
あくまで技術検証なので、実運用に向けた細かい話は一旦置いておき、**「既存のRails アプリとローカル環境で、ブラウザ通知の疎通確認ができること」**を最速で目指しました。
- やること: ローカルの開発環境からブラウザに通知を飛ばす
- やらないこと: ユーザーごとのトークンをデータベースに保存するための諸々の設計や実装、および本番運用時の考慮
2. 使用した技術・環境
-
Ruby on Rails (バージョン 8.0)
- 既存のアプリケーションを想定しています。
-
Gem: web-push
- 通知の送信や通知に必要な VAPID キーの生成に使用
-
Gem: localhost
- ローカル開発環境を手軽に HTTPS 化するために使用
-
開発環境: macOS (バージョン 15.7)
- ※ここが後述する証明書周りの設定に関わってきます
3. 【ハマりポイント1】ローカル開発環境のHTTPS化
Webプッシュ通知に必要な Service Worker を動かすためには、セキュリティ上の制約で HTTPS 環境が必須となります。
Railsのローカル環境(localhost:3000)をHTTPS化するために、今回は localhost gemを採用しました。
導入自体は Gemfile に追加して bundle install するだけなのですが、macOSの場合、証明書周りでハマりポイントがありました。
症状
Gemを入れただけでは、ブラウザが「保護されていない通信」と判断してしまい、Service Workerが登録できませんでした。
解決策:Keychain Accessでの設定
macOSの「キーチェーンアクセス.app」を開き、localhost gemが生成した証明書を追加して、「常に信頼」する設定に変更する必要がありました。
- キーチェーンアクセスを開く
- 証明書("localhost.crt")を追加する
- 証明書は、puma を使用している場合は、localhost gem を追加してから、起動すると自動で生成されます。生成された証明書は、puma を実行するユーザーのホームディレクトリ内("~/.local/state/localhost.rb/")に保存されています。
- 追加は、"Certificate" タブの画面にファイルをドラッグしてドロップすれば追加できます
- 追加した証明書を開いてから、「信頼」設定を開き、「常に信頼(Always Trust)」に変更する
- アプリケーションの起動オプション、もしくは、設定を変更する
- puma の場合、"bundle exec puma --bind ssl://0.0.0.0:9292" のようにして、9292 番ポートでリクエストを受け取るようにする
これで無事にローカルでもHTTPSでアクセスできるようになりました。
4. 最小限の実装手順
環境が整ったので、実装を進めます。
**(0) 事前準備
Rails 7.2 で、Rails::PwaController が追加されていますが、その他、"manifest.json" や "service_worker.js" などが追加されていないので、追加しました。
{
"name": "Sample",
"icons": [
{
"src": "/icon.png",
"type": "image/png",
"sizes": "512x512"
},
{
"src": "/icon.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "maskable"
}
],
"start_url": "/",
"display": "standalone",
"scope": "/",
"description": "Sample",
"theme_color": "red",
"background_color": "red"
}
// Add a service worker for processing Web Push notifications:
//
self.addEventListener("push", async (event) => {
// const { title, options } = await event.data.json();
// event.waitUntil(self.registration.showNotification(title, options));
});
//
// self.addEventListener("notificationclick", function(event) {
// event.notification.close()
// event.waitUntil(
// clients.matchAll({ type: "window" }).then((clientList) => {
// for (let i = 0; i < clientList.length; i++) {
// let client = clientList[i]
// let clientPath = (new URL(client.url)).pathname
//
// if (clientPath == event.notification.data.path && "focus" in client) {
// return client.focus()
// }
// }
//
// if (clients.openWindow) {
// return clients.openWindow(event.notification.data.path)
// }
// })
// )
// })
追加したファイルが配信されるように "config/routes.rb" に以下を追加しました。
get 'manifest' => 'rails/pwa#manifest', as: :pwa_manifest
get 'service-worker' => 'rails/pwa#service_worker', as: :pwa_service_worker
そして、"manifest.json" を配信するために "app/views/layouts/application.html.erb" に以下を head タグの中に追加しました。
<%= tag.link rel: 'manifest', href: pwa_manifest_path(format: :json) %>
ここで、"https://localhost:9292" にアクセスすると、アドレスバーにアプリケーションのインストールアイコンが出るようになりました。
(1) web-push gemの導入
まずは web-push gemをインストールし、VAPIDキー(公開鍵と秘密鍵)を生成します。生成したキーは、後で利用するために ノート.app に記録しておきました。
$ bundle exec rails console
app(dev)> vapid_key = WebPush.generate_key
app(dev)> puts vapid_key.public_key, vapid_key.private_key, Base64.urlsafe_decode64(vapid_key.public_key).bytes
=> nil
<VAPID 公開鍵>
<VAPID 秘密鍵>
<JavaScript 内で使用する VAPID 公開鍵>
(2) Service Workerで通知の受け取り
"app/views/pwa/service_worker.js.erb" を変更して、通知を受け取るようにしました。
self.addEventListener("push", async (event) => {
const { title, body } = await event.data.json();
event.waitUntil(self.registration.showNotification(title, { body }));
});
**(3) クライアント側の購読処理を追加する **
今回は、ブラウザ側でプッシュ通知を許可(購読)した際に発行される endpoint や auth キーなどの情報は、JavaScriptコンソールに出力するという割り切った実装にします。あるページにボタンを置いて、そのボタンの onClick ハンドラで、以下の registerServiceWorker を実行するなどにしました。
const validPublicKey = new Uint8Array(<JavaScript 内で使用する VAPID 公開鍵>);
const registerServiceWorker = async () => {
if ('serviceWorker' in navigator) {
try {
// 登録済みのサービスワーカーの取得、未登録であれば登録
const registration = await navigator.serviceWorker.getRegistration('/service-worker.js') ?? await navigator.serviceWorker.register('/service-worker.js');
// 通知の購読
await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: validPublicKey });
// 本来なら、ここで購読内容をバックエンド側に送る
console.log((await registration.pushManager.getSubscription()).toJSON());
} catch (error) {
// service worker 登録に失敗、もしくは、push 通知の購読に失敗
console.error('Service worker registration or push subscription failed:', error);
}
} else {
// serivce worker 非対応
}
};
上記のコードが実行されると以下のような通知を受信する許可を求めるダイアログが出力されるので、許可しました。
許可すると JavaScript コンソールに以下のように購読内容な出力されます。後で使用するので、別途、ノート.app などに記録しておきました。
(4) 【ハマりポイント2】ブラウザ(DevTools)の設定
Service Workerのコードを修正しても、ブラウザをリロードしただけでは更新されないことがあり、ここでも少し詰まりました。
解決策
Chrome の Developer Toolsを開き、Application タブ > Service Workers にある 「Update on reload」 にチェックを入れることで、リロード時に即時反映されるようになります。
5. 通知の送信
rails console から手動で通知を送ってみます。
$ bundle exec rails console
app(dev)* WebPush.payload_send(
app(dev)* message: { title: 'テスト通知', body: 'テスト通知です。' }.to_json,
app(dev)* endpoint: '<前手順でJavaScriptコンソールに表示した値>',
app(dev)* p256dh: '<前手順でJavaScriptコンソールに表示した値>',
app(dev)* auth: '<前手順でJavaScriptコンソールに表示した値>',
app(dev)* vapid: {
app(dev)* subject: 'mailto:sample@example.com',
app(dev)* public_key: '<WebPush.generate_keyで生成した値>',
app(dev)* private_key: '<WebPush.generate_keyで生成した値>'
app(dev)* }
app(dev)> )
=> #<Net::HTTPCreated 201 Created readbody=true>
実行した結果...
無事に通知が届きました!
6. おわりに
今回はスパイク実装ということで、購読情報の保存などの面倒な部分をスキップし、**「まずは動く環境を作る」**ことにフォーカスしました。
実際にやってみて、実装コードそのものよりも、**「macOSでの証明書の信頼設定」や「Service Workerの更新挙動」**といった環境周りの仕様理解に時間がかかると感じました。
もし今後、実際に実装する際に、この検証ログが少しでも役に立てば嬉しいです。






