0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

通知を、製品ごとに作らない — メール・Slack・LINE・Webhookを1つの基盤にまとめた notify

0
Last updated at Posted at 2026-09-23

通知を、製品ごとに作らない — メール・Slack・LINE・Webhookを1つの基盤にまとめた notify

上原正吉(EarthLink Network Co., Ltd.)。Claude Codeを開発の主体に据え、20を超えるプロダクトを1人で同時に開発・運用しています。これは、その現場の実測記です。

通知を、製品ごとに作らない — メール・Slack・LINE・Webhookを1つの基盤にまとめた notify

結論

  • 何を作ったか: メール・Slack・LINE・Webhook(システム間の自動通知)への通知を、1つのAPIで送れる共通基盤 notify です。社内の各製品は、通知の仕組みを自前で作らず、SDK(Software Development Kit、外部機能を呼び出すための開発部品)とプロジェクト単位のAPIキーだけで送れます。
  • なぜ作ったか: 複数の製品が、メール送信やSlack通知をそれぞれ別々に実装していたからです。似たコードが製品の数だけ増え、LINE通知に至ってはどの製品も未実装のままでした。
  • 何が要点か: 通知は「送れること」より「送りすぎないこと・送り漏れないこと」が難しい機能です。同じ送信を二重に飛ばさない仕組みと、失敗したら次の送信先へ自動で切り替える仕組みを、各製品ではなく基盤の側で持ちます。いまは13のプロジェクトがこの基盤の上で通知を送っています。

本文(読了 約14分)

社内の製品を増やしていくと、必ず全部の製品に要る機能があると気づきます。ログイン、課金、そして通知です。ログインは認証基盤(ELN ID)に、課金は課金基盤にまとめてきました。この記事は、その並びの3つ目、通知基盤 notify の紹介です。

notify の製品ページ。メール・Slack・LINE・Webhook の共通通知基盤(https://www.eln.ne.jp/products/notify の実画面)

どの製品にも通知はあるのに、毎回作っていた

発端は、通知まわりの実装を並べて見たことです。サポート系の製品も、監視系の製品も、コーポレートサイトも、メールを送るためにAmazon SES(Simple Email Service、メールを送受信するAWSのサービス)を、Slackに知らせるためにWebhookを、それぞれ自分のコードとして持っていました。書いた人も時期も違うので、細部の挙動もばらばらです。

つまり、メール送信のたびに似たコードが増えていたわけです。そして「あると便利」と皆が言うLINE通知は、どの製品も後回しにして、全社どこにも実装がありませんでした。1つの製品のためにLINE連携を作り込むのは割に合わない。でも全製品で使えるなら話が変わります。通知を製品の一部ではなく、それ自体を1つの製品として扱うことにしました。

2026年6月末に、まずコーポレートサイトからメール送信部分をライブラリとして切り出しました。この時点ではメール専用です。半月後の7月16日に「notifyはライブラリではなく、画面とAPIを持つ1つの製品にする」と設計判断(ADR)を書き、翌17日に4チャネル対応・送信の司令部・管理画面・SDKまでを一気に作りました。以後も署名とテンプレート管理(8月)、認証メールの10言語対応(9月)、Slackの複数チャネル対応(9月)と、利用側の要求が来るたびに基盤側へ機能を足しています。

notifyでできること

利用する側から見ると、notify は次の形をしています。

  • 1つのAPIで4つのチャネルへ送れます。 メール・Slack・LINE・Webhookを、同じ呼び出し方で扱います。送信のAPIはただ1本(POST /api/v1/send)で、呼び出す側は「どの手段で届けるか」を毎回作り込みません。
  • 送信先に優先順位をつけ、失敗したら次へ自動で切り替えます。 依頼に並べたチャネルの先頭から順に試し、成功した時点で残りへの切り替えを止めます。先頭のチャネルが落ちていても、通知そのものが消えることを防ぎます。
  • 同じ送信を二重に飛ばしません。 送信の依頼に送信ID(同じIDの依頼を2回受け取っても、1回しか送らないための目印)を付けられます。同じIDで再送された依頼は、基盤が安全に無視します。通知の呼び出し側は、失敗時にためらわず再試行できます。
  • 宛先は3つの形で指定できます。 メールアドレスなどの直接指定のほかに、「このユーザーへ」というユーザー参照で送れます。ユーザー参照は本人確認済みの宛先にだけ解決され、未確認の宛先へは送りません(本人が2段階で同意した宛先にだけ送る、いわゆるdouble opt-in)。Slackは、あらかじめ登録したチャネルを名前で指定します。
  • メールのテンプレートと署名を基盤側で管理します。 組織への招待、お問い合わせの受付通知といった6種類の定型メールを既定で持ち、プロジェクト独自のテンプレートも追加できます。署名は「本文→プロジェクト署名→会社共通署名」の順で自動合成されるので、製品ごとに文面へ署名を貼り付ける作業が消えます。
  • 認証メールは10言語で出し分けます。 英語・日本語・簡体字中国語・韓国語・スペイン語・ポルトガル語・フランス語・ドイツ語・アラビア語・ヒンディー語です。利用者の言語設定に応じて、同じイベントでも言語別のテンプレートが選ばれます。
  • 迷惑メール対策を内蔵しています。 届かないアドレス(bounce)や苦情(complaint)は自動で送信停止リストに入り、メールにはワンクリックで配信解除できるヘッダ(List-Unsubscribe)が付きます。送りすぎて嫌われる事故を、各製品の注意ではなく基盤の仕組みで防ぎます。
  • 誰が・どのプロジェクトで・何を送ったかが管理画面に残ります。 プロジェクト管理・APIキーの発行と失効・チャネル設定・送信ログ・テスト送信を1つの管理画面で扱えます。ログインは自社の認証基盤(ELN ID)です。

notify の主要機能。マルチチャネル配信・優先順位付けとフォールバック・二重送信防止・迷惑メール対策・管理画面(https://www.eln.ne.jp/products/notify の実画面)

呼び出し側のコードは、これだけです。

const client = createNotifyClient({ baseUrl, apiKey });
await client.send({
  targets: [{ userRef: 'user-123' }, { address: 'ops@example.com' }],
  channels: ['email', 'slack'],
  topic: 'billing',
  message: { subject: '請求確定', text: '今月の請求が確定しました。' },
});

SDKの実行時依存は入力検証ライブラリ1つだけに抑えてあります。一時的な障害(5xx・ネットワークエラー)は0.2秒から倍々に間隔を延ばしながら自動で計3回まで試行し、呼び出し側の間違い(4xx)は再試行せず即座にエラーにします。送信IDを指定しなければSDKが自動で採番するので、「付け忘れて二重送信」も起きません。

通知を製品ごとに作らず1つの基盤で送る — Before(製品ごとの自前実装・LINEはどこにも無い)とAfter(各製品はSDKとAPIキーだけ・notifyが送信IDとフォールバックを持つ)の対比(実装済みの構成から作図)

設計で一番こだわった点 — 「業務上の失敗」を例外にしない

通知には、成功でも通信エラーでもない結果があります。「同じ送信IDなので送信を省略した(duplicate)」「配信停止の相手なので送らなかった(rejected)」です。これは異常ではなく、基盤が正しく仕事をした結果です。

notify のAPIは、これらをエラー用のHTTPステータス(4xx)ではなく、正常応答(200)の本文として返します。

POST /api/v1/send
200 { status: "processed" }   // 送った
200 { status: "duplicate" }   // 同じ送信IDなので省略した(正常)
200 { status: "rejected" }    // 配信停止先なので送らなかった(正常)

なぜこうしたか。業務上の失敗を4xxで返すと、多くのHTTPクライアントはそれを一律の「通信エラー」として例外にまとめてしまい、呼び出し側は「二重送信を防げた」のか「本当に失敗した」のかを区別できなくなります。特に、再試行のたびに duplicate が偽の失敗として例外を投げるのは致命的です。例外は通信と認証のエラーだけに限定し、業務の分岐は本文で表す。この設計理由は基本設計書に明文化して、後から来た人が「4xxに直そう」と善意で壊さないようにしてあります。

送らなかった理由も、自由文ではなく決まった語彙で返します。topic_opt_out(配信停止済み)・binding_not_verified(宛先が本人未確認)・channel_config_missing(チャネル未設定)・slack_channel_not_registered(未登録のSlackチャネル宛)。呼び出し側はこの語彙で分岐でき、ログを見る人は理由を推測しなくて済みます。

実際にこの区別が効いている例が、コスト監視製品のアラート送信です。そこでは応答の duplicate を「送信済みだが今回は新規ではない」として扱い、「届いたか」と「新しく送ったか」を別のフラグで管理しています。業務の分岐が本文に残っているから、呼び出し側がこういう使い方を組み立てられるわけです。

送信IDの中身 — 「再送しても安全」を成立させる作り込み

送信IDは「同じIDなら2回送らない」という単純な約束ですが、単純な実装では守れません。作り込んだのは3点です。

  • 予約は早い者勝ちで記録します。 同じ(プロジェクト、送信ID)の組はデータベースの条件付き書き込みで先着1件だけが通り、2件目以降は送信処理に入らず duplicate で返ります。
  • 送信中に例外で落ちたら、予約を取り消します。 これをしないと、送っていないのに予約だけが残り、同じIDが永久に duplicate になる「毒入り」状態になります。再送の安全装置が、再送を永久に止める装置に化けるわけです。
  • 全部の宛先が一時的な障害で終わったときだけ、IDを解放します。 相手側の5xxやネットワーク断なら、時間を置いた再送で成功する見込みがあるからです。逆に、1件でも送信成功・配信停止・恒久的な失敗があれば、IDは消費したままにします。再送しても結果は変わらないからです。

この判断を成立させるために、チャネルごとにばらばらな「失敗」の言葉も揃えました。HTTPの5xxとネットワークエラーは再試行可能、SlackのAPIが返す論理エラーは再試行不可、LINEの流量制限(429)は再試行可能。各チャネルの実装がこの共通の目印を返し、送信の司令部はそれだけを見てIDの扱いを決めます。

外へ飛ぶ機能ほど、疑ってかかる

通知は「外に飛ぶ」機能です。だからこそ、不審な入力を拒み、異常を明示することを徹底しました。

  • Webhookの送信先には、内部ネットワーク宛のアドレスを指定できません。 サーバーに意図しない内部宛先へ通信させる攻撃(Server-Side Request Forgery、SSRF)への対策です。localhost・プライベートアドレス帯・クラウドの内部設定情報が取れる特殊アドレス(169.254.169.254)・URLに埋め込まれた認証情報などを送信前に弾き、なぜ弾いたかの理由文字列を結果に残します。
  • Webhookには署名を付けます。 送信時刻と本文から計算した署名をヘッダに載せ、受け取る側が「本当にnotifyからの通知か」を検証できるようにしています。逆に、相手サーバーの応答本文は結果に載せません。通知の応答を悪用して内部の情報を読み出す攻撃の芽を摘むためです。
  • APIキーは平文を保存しません。 キーは nk_ で始まる32文字で、データベースに残るのはハッシュ(元の値に戻せない変換値)だけ。平文は発行の瞬間に一度だけ表示されます。管理画面のAPIも、SlackのトークンやLINEのアクセストークンといった秘密値そのものは返さず、「設定済みかどうか」だけを返します。
  • 設定が足りなければ、黙って代わりの値で動いたりせず、その場で止まります。 環境変数13個はどれが欠けても起動時または送信時に明示的にエラーになります。利用する製品の側も同じ方針で、接続先とAPIキーが未設定なら送信時点で「未設定です」と例外を投げます。通知基盤の最悪の状態は「設定ミスで黙って送らない、または送りすぎる」ことだからです。

実際の使われ方 — 5つの製品が乗っている

2026年9月時点で、この基盤には13のプロジェクトが登録され、性格の違う製品が実際に通知を流しています。

  • ヘルプデスク製品は、キャンペーンメールの配送をnotifyに委ね、担当者向けのSlack通知も送っています。
  • コスト監視製品は、コストの異常アラートを「Slack→メール→汎用Webhook」の3段構えで送ります。Slackが落ちていてもメールが出る、という多段の切り替えを、製品側は送信先を並べるだけで手に入れています。
  • **認証基盤(ELN ID)**は、会員登録の確認メールとパスワード再設定メールをnotifyのテンプレートで送ります。この経路があるので、認証メールの文面変更や多言語化に認証基盤側の変更が要りません。
  • コーポレートサイトは、お問い合わせフォームの受付通知に使っています。notifyの切り出し元でもあります。
  • 旗艦のAIワークスペース製品も、メール送信をnotifyへ委ねると決めてあります(結線は接続の前提条件が揃うのを待っている準備状態です)。

基盤を作ってからも、利用側の要望は続きます。「Slackのチャネルを画面で選びたい」が来たので、Slackアプリのインストールを一次化して登録済みチャネルから選ぶ方式に変えました。「認証メールを利用者の言語で出したい」が来たので、テンプレートを言語別に持てるようデータ構造を拡張しました。共通基盤は作って終わりではなく、利用側の圧力で育っていきます。

開発の裏側 — 共通リポジトリの合意を、1度だけ破った

notifyの実装は、社内の共通パッケージ置き場(monorepo)に入っています。ここには元々「純粋なライブラリだけを置く。動き続けるサービスは置かない」という合意がありました。notifyはAPIと管理画面を持つサービスなので、本来はこの合意に反します。

それでも収容すると決めて、設計判断の文書に代替案ごと記録しました。コーポレートサイトの中に置き続ける案は責務が膨らみ続けるので却下。インフラ構成専用のリポジトリに置く案は規約違反なので却下。専用リポジトリを新設する案は、SDKと型定義を共通パッケージとして配る利点を失うので却下。「合意を破る」という判断ほど、なぜ破ったかを文書に残す価値があります。半年後の自分が「なぜここにあるんだ」と混乱しないためです。

もう1つ、配る側のパッケージは純粋なライブラリの原則を守りました。メール送信・HTTP・時刻・ID採番といった外部への依存は全て注入方式にし、データベース実装は管理画面側に置いています。この分離のおかげで、テスト658件(基盤422件・SDK 17件・管理画面219件)がAWSの実物なしで数秒で回ります。テストが速いから、機能追加のたびに全件回して壊れていないことを確かめられます。

規模感を数字で書いておくと、基盤本体の実装が約5,400行、管理画面が約7,500行、SDKが約450行。設計判断の文書(ADR)が6本、基本設計書が2本。切り出しから約2か月半でここまで来ました。

障害から学んだこと — 対策は、設計しただけでは効かない

この基盤は最初、自社の拠点サーバーからトンネル経由で配信していました。2026年8月30日、その管理画面が約104秒間、完全に落ちました。

調べると、アプリケーションは正常に動き続けていました。落ちたのは経路です。拠点のトンネル接続4本が同時に切れていました。皮肉なのは、同じ型の障害を7月に一度経験していて、対策(別方式への切り替え)も設計済みだったことです。ところが、その対策の変更は提案のまま取り込まれておらず、本番には入っていませんでした。対策は、設計しただけでは効きません。本番に入って初めて対策です。

翌日、配信をクラウドのマネージド配信(AWS Amplify)へ移すと即断しました。移行では3つの制約に連続でぶつかっています。SSR(サーバー側レンダリング)の手動デプロイに対応していない。ビルド済みの成果物を渡したいのにNext.jsを検出して勝手にビルドしようとする。実行環境のファイルシステムが読み取り専用でキャッシュ書き込みに失敗する。それぞれ、ダミーの実行構成を分離する・ビルドは自前のCIで1回だけ行いAmplify側は展開だけにする・キャッシュの書き込み先を一時領域へ向ける、という回避で通しました。この「ビルドは1回だけ、配信環境では展開だけ」という構成は、その後ほかの製品の配信にも流用しています。

細かい取り違えも記録に残しています。Slackへ送るつもりで汎用Webhookチャネルを設定し、Slack側が受け取れない形式で送っていた失敗。これは「Slackにはslackチャネル、汎用webhookは別物」という1行の教訓ですが、次に踏む人を確実に減らします。

扱うのは送信だけ

なお、notify が扱うのは送信だけです。メールを受け取ってサポートのチケットにする側は、別の製品(ヘルプデスク基盤)が持ちます。機能名ではなく通信の方向で製品の境界を分けたわけですが、この境界を引くまでの議論と、共通リポジトリの合意をどう改訂したかは、別の記事で詳しく書いています。

転用できる教訓

  • どの製品にも要る機能(ログイン・課金・通知)は、2つ目の製品を作った時点で基盤化を検討する価値があります。1製品では割に合わない機能(LINE連携など)も、全製品で使うなら成立します。
  • 業務上の失敗(重複・拒否)はHTTPのエラーにせず、正常応答の分岐として返します。例外は通信と認証のエラーだけにします。理由は決まった語彙で返し、呼び出し側が分岐できるようにします。
  • 通知の呼び出し側には送信IDを持たせ、再試行を安全にします。「送りすぎない」は呼び出し側の注意ではなく、基盤の仕組みで守ります。そのとき、例外時の予約取り消しと一時障害時のID解放まで作り込まないと、安全装置が再送を止める装置に化けます。
  • 外へ飛ぶ機能には、送信先の検証と「設定不足なら止まる」を最初から入れます。
  • 障害対策は本番に入って初めて対策です。設計済み・提案済みの対策が放置されていないかは、仕組みで確かめる必要があります。

この共通基盤(認証・通知・課金など)についての記事は、開発の経緯・どんな機能があるか・どう実装したかを、順次シリーズとして公開していきます。
興味のある方は、ぜひ「いいね」と記事の購読をお願いいたします。

ここで紹介した基盤の上で動く自社プロダクトは https://www.eln.ne.jp/products にまとめています。

筆者について

上原正吉。EarthLink Network Co., Ltd. でAI開発をしています。2025年からClaude Codeを開発の主体に据え、今は20を超えるプロダクトを1人で同時に開発・運用しています。この連載では、その現場で実際に起きたこと(うまくいったことも、失敗も)を、数字と一緒に書いていきます。

また、AIで業務や開発を組み替えたい会社・チーム向けに、AI活用のコンサルティングも受け付けています。ご相談は www.eln.ne.jp からどうぞ。


EarthLink Network は、会社の全業務を AI で回すために、必要になったものを自社で作っています。いま作っているプロダクトの一覧と概要は、こちらにまとめています。

→ EarthLink Network が自社でつくっている18のプロダクト

会社と各プロダクトの詳細は、公式サイト www.eln.ne.jp をご覧ください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?