サーバーを立てずに「AからBへ通知したい」を実現する
個人開発をしていると、「スマホで何か操作したらPCに通知したい」「バッチ処理が終わったら自分専用の何かに知らせたい」といった、AからBへちょっとした情報を飛ばしたい場面によく出会います。
本格的にやるならWebSocketサーバーを自前で立てる、あるいはFirebaseやPusherのようなBaaSを使う、という方法もあります。ですが、個人開発の小さな用途にそこまでの構成は大げさです。アカウント登録、APIキーの管理、サーバー費用……乗り越えるハードルの割に、やりたいことは「メッセージを1つ送って、もう片方が受け取る」だけだったりします。
そんなときに便利なのがntfy.shです。実際、自作のChrome拡張QR Send(https://qrsend.app)でもこの仕組みを使っています。
ntfy.shの仕組みはシンプル
ntfy.shは無料で使えるpub/sub(publish/subscribe)サービスです。仕組みは驚くほどシンプルで、次の2つだけで成立します。
- 送る側: 好きな「トピック名」に向けてPOSTする
- 受ける側: 同じトピック名にGETする(あるいはSSE/WebSocketで購読する)
アカウント登録もAPIキーの発行も不要です。トピック名は文字列を決めるだけで、事前登録すら要りません。ではcurlで送ってみましょう。
curl -d "デプロイが完了しました" https://ntfy.sh/your-topic-name-here
受信側はGETで直近のメッセージをポーリングして取得できます。
curl -s "https://ntfy.sh/your-topic-name-here/json?poll=1"
これだけで、AからBへの通知が成立しましたね。サーバーを立てる必要も、常時起動するプロセスも(受信側のポーリング以外は)必要ありません。簡単ですね。
トピック名は「推測困難なランダム文字列」にする
ここで気をつけたいのがトピック名の決め方です。ntfy.shのトピックは、名前さえ知っていれば誰でもpublish/subscribeできる仕組みになっています。つまりalertsやmy-notifyのような分かりやすい名前にしてしまうと、同じ名前を使った他の人のメッセージが届いたり、逆に自分のメッセージを他人に見られたりする可能性があります。
そこで、crypto.randomUUID()のようなAPIで生成した長いランダム文字列をトピック名にします。
const topic = crypto.randomUUID();
// 例: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
これだけの長さの文字列を偶然言い当てられる確率は実質ゼロです。URLを知っている人だけが読み書きできる「実質的にプライベートな通知チャンネル」として使えます。
ただ、ここは正直に書いておきます。ntfy.shの無料枠のトピックは、暗号化された非公開チャンネルではなく、あくまで「名前を知っていれば誰でもアクセスできる公開のエンドポイント」です。ランダムな名前で推測されにくくなっているだけで、本質は公開の場所だという前提を忘れてはいけません。パスワードやAPIキー、個人情報など、漏れて困る情報を流すのはやめておきましょう。あくまで「URLの中身」や「処理が終わったことを知らせるだけの短いメッセージ」など、万一見られても実害が小さいものに使うのが安全な運用だと思います。
JavaScript(fetch)での送信・購読の例
ブラウザやChrome拡張から使う場合は、fetchだけで完結します。まずは送信側です。
async function publish(topic, message) {
await fetch(`https://ntfy.sh/${topic}`, {
method: "POST",
body: message,
});
}
publish("f47ac10b-58cc-4372-a567-0e02b2c3d479", "PC作業が完了しました");
受信側は一定間隔でポーリングして、新着があれば処理する形にします。
let lastMessageId = null;
async function pollOnce(topic) {
const res = await fetch(`https://ntfy.sh/${topic}/json?poll=1&since=5m`);
const text = await res.text();
const messages = text
.trim()
.split("\n")
.filter(Boolean)
.map((line) => JSON.parse(line));
for (const msg of messages) {
if (msg.id === lastMessageId) continue;
lastMessageId = msg.id;
console.log("受信:", msg.message);
}
}
setInterval(() => pollOnce("f47ac10b-58cc-4372-a567-0e02b2c3d479"), 30000);
since=5mを付けているのは、起動直後に古いメッセージまで大量に取得してしまわないようにするためです。ポーリング間隔は用途次第ですが、個人利用の通知であれば30秒程度で十分実用的です。リアルタイム性がどうしても欲しければSSEやWebSocketでの購読もできますが、個人開発の軽い用途にそこまでは要らないと思います。
メッセージは一定時間で消える
ntfy.shに溜まったメッセージは、無料枠では一定時間が経過すると自動的に削除されます。つまりこのサービスは「ずっと残るデータベース」ではなく、「今すぐ届けるための一時的な中継所」だと考えるのが正確です。
これは制約であると同時に、実は都合の良い性質でもあります。履歴を自分で管理・削除する手間を気にせず、使い捨てのメッセージを流せるからです。逆に言えば、後から参照したい記録や重要なログを保存する用途には向いていません。「今すぐ相手に届けばよい、届かなかったら諦めてよい」という通知にこそ向いたサービスですね。
まとめ
トピック名にPOSTすれば送信、GETすれば購読。ntfy.shで必要なのは結局その2つだけで、アカウントもサーバーも要りません。ただしトピック名を知っていれば誰でも読み書きできる仕組みである以上、ランダムな名前にしても本質は公開のエンドポイントだという前提は忘れないほうがいいでしょう。機密情報は流さず、消えても困らない一時的な通知の中継として使うのが向いています。


コメント