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

notify です。社内の各製品は、通知の仕組みを自前で作らず、SDK(Software Development Kit、外部機能を呼び出すための開発部品)とプロジェクト単位のAPIキーだけで送れます。社内の製品を増やしていくと、必ず全部の製品に要る機能があると気づきます。ログイン、課金、そして通知です。ログインは認証基盤(ELN ID)に、課金は課金基盤にまとめてきました。この記事は、その並びの3つ目、通知基盤 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 は次の形をしています。
POST /api/v1/send)で、呼び出す側は「どの手段で届けるか」を毎回作り込みません。
呼び出し側のコードは、これだけです。
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が自動で採番するので、「付け忘れて二重送信」も起きません。

通知には、成功でも通信エラーでもない結果があります。「同じ送信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なら2回送らない」という単純な約束ですが、単純な実装では守れません。作り込んだのは3点です。
duplicate で返ります。duplicate になる「毒入り」状態になります。再送の安全装置が、再送を永久に止める装置に化けるわけです。この判断を成立させるために、チャネルごとにばらばらな「失敗」の言葉も揃えました。HTTPの5xxとネットワークエラーは再試行可能、SlackのAPIが返す論理エラーは再試行不可、LINEの流量制限(429)は再試行可能。各チャネルの実装がこの共通の目印を返し、送信の司令部はそれだけを見てIDの扱いを決めます。
通知は「外に飛ぶ」機能です。だからこそ、不審な入力を拒み、異常を明示することを徹底しました。
nk_ で始まる32文字で、データベースに残るのはハッシュ(元の値に戻せない変換値)だけ。平文は発行の瞬間に一度だけ表示されます。管理画面のAPIも、SlackのトークンやLINEのアクセストークンといった秘密値そのものは返さず、「設定済みかどうか」だけを返します。2026年9月時点で、この基盤には13のプロジェクトが登録され、性格の違う製品が実際に通知を流しています。
基盤を作ってからも、利用側の要望は続きます。「Slackのチャネルを画面で選びたい」が来たので、Slackアプリのインストールを一次化して登録済みチャネルから選ぶ方式に変えました。「認証メールを利用者の言語で出したい」が来たので、テンプレートを言語別に持てるようデータ構造を拡張しました。共通基盤は作って終わりではなく、利用側の圧力で育っていきます。
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 が扱うのは送信だけです。メールを受け取ってサポートのチケットにする側は、別の製品(ヘルプデスク基盤)が持ちます。機能名ではなく通信の方向で製品の境界を分けたわけですが、この境界を引くまでの議論と、共通リポジトリの合意をどう改訂したかは、別の記事で詳しく書いています。
この共通基盤(認証・通知・課金など)についての記事は、開発の経緯・どんな機能があるか・どう実装したかを、順次シリーズとして公開していきます。 興味のある方は、ぜひ「いいね」と記事の購読をお願いいたします。
ここで紹介した基盤の上で動く自社プロダクトは 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 をご覧ください。