決済は1か所、機能は各アプリ — 複数SaaSの課金をまとめる ELN Billing

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

決済は1か所、機能は各アプリ — 複数SaaSの課金をまとめる ELN Billing

決済は1か所、機能は各アプリ — 複数SaaSの課金をまとめる ELN Billing

結論

  • 何を作ったか: 複数の自社SaaS(Software as a Service、インターネット経由で利用するソフトウェア)の課金を1か所に集約する基盤 ELN Billing です。Stripe(決済サービス)を包み、契約・クーポン・都度課金・紹介報酬・返金までを1つのAPI群で扱います。決済や解約が起きると、署名付きのWebhook(サーバー間の自動通知)で各サービスへ知らせます。
  • なぜ作ったか: 決済まわりの実装は、1つのサイトだけでサブスクリプション関連のファイルが30を超えていました。サービスを増やすたびにこれを複製するのは現実的ではありません。
  • 何が要点か: 集約サービスは「何を持つか」より「何を持たないか」を決めることが肝です。ELN Billingは「契約しているか」の正本だけを持ち、「契約しているから何ができるか」の解釈は各サービスに残しました。開発6か月・コミット172件・仕様書17本で、いまはpre-GA版が本番で動いています。

本文(読了 約14分)

前回は通知基盤の話をしました。今回は同じ並びの課金です。どのSaaSにも必ずある機能を、製品ごとに作らず基盤に寄せる話の2つ目です。

ELN Billing のサイト。全プロダクト共通の課金基盤として、契約・クーポン・都度課金を単一APIで扱う(https://elnbilling.eln.ne.jp の実画面)

30ファイルの複製は、していられない

発端は実装量の確認でした。ある既存サイトの決済まわりを数えると、サブスクリプション関連だけでファイルが30を超えていました。Stripeとの連携、契約状態の管理、クーポン、返金の運用。これを新しいSaaSを立ち上げるたびに複製していくのは、書く量の問題だけではありません。返金や課金トラブルの対応が各サイトに分散し、利用者もサービスごとに別の管理画面を見せられます。

目指す形は、Appleの App Store と同じ考え方にしました。決済は1か所、機能は各アプリ。支払いと契約の管理は基盤が引き受け、その先の「何ができるか」は各サービスが決める、という分担です。

ELN Billingでできること

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

  • 契約・クーポン・都度課金を、1つのAPI群で扱えます。 各サービスは決済画面もStripe連携も自前で持ちません。決済ページが欲しければ、プランIDと戻り先URLを渡すだけでStripeの決済ページのURLが返ります。登録SaaS向けの公開APIは22本あり、サブスクリプションの照会・解約、クーポン検証、クレジットの購入・消費・残高照会、紹介コードの発行と成約の記録、返金までを覆っています。
  • APIキーはサービス単位です。 キーで呼び出し元のサービスが特定され、応答は自動的に自サービスの分に絞られます。他サービスの契約情報は、キーを持っていても見えません。
  • 決済のできごとは、署名付きWebhookで各サービスへ通知します。 契約成立・更新・解約・支払い失敗・クレジット残高の低下など14種類のイベントを、改ざん検知つきで受け取れます。
  • 利用者向けのセルフサービス画面を基盤側が持ちます。 自分の契約の確認・解約・クーポン適用ができる画面で、日本語・英語・中国語・韓国語・タイ語・スペイン語の6言語に対応しています。各サービスが解約画面を作る必要はありません。
  • 運営側は、全サービスの売上を1つの管理画面で見られます。 サービス・プラン・クーポン・契約・クレジット・紹介の管理と、MRR/ARR(月間・年間の繰り返し売上)の分析画面があります。管理画面は利用者向けの画面と物理的に分離し、別ドメイン・別配信にしています。
  • プランの登録は各サービスが自分で行います。 各サービスが自分のAPIキーでプランを作成・更新します。基盤の管理画面からプランを作らないのが公式ルールで、プランの改定は同じIDの上書きではなく、別IDの新版を作って連鎖でつなぐ版管理方式です。

ELN Billing の主な機能。単一API・署名付きWebhook・自動リトライ・Stripe基盤・複数SaaS横断・購読状態の正本管理(https://elnbilling.eln.ne.jp の実画面)

一番大事な設計判断 — 「プランで何ができるか」を持たない

作っている途中、あるサービスから「プランごとの機能上限(たとえば実行間隔の下限値)を基盤側で持って、強制してほしい」という依頼が来ました。課金の基盤なのだから、プランと機能の対応表もそこに置くのが自然に聞こえます。

私はこれを実装しませんでした。基盤がその値の意味を理解した瞬間、基盤はそのサービス固有の知識を背負います。次のサービスが別の上限の概念を持ち込めば、また基盤に定義が増える。課金基盤は「壊れたら全サービスの決済が止まる」位置にあるので、そこに各サービスの仕様が雪だるま式に溜まっていく未来は避けたいのです。

代わりに、プランには各サービスが自由な形式で値を書き込める欄を用意しました。監視系のサービスなら「監視対象は50個まで・最短実行間隔は60秒」、AIワークスペースなら「クレジット追加購入の単価と、作成数の上限なし」のように、各サービスが自分の言葉で書きます。機能の意味・値・強制は各サービスが所有し、基盤は中身を解釈せずに配るだけです。

この分担は、やってはいけないことの形でも文書にしています。「個別サイトに価格をハードコードしない」「基盤で権限・機能制限を定義しない」「プラン名の文字列一致でプランを識別しない(必ずプランIDで紐付ける)」。「契約しているか」までが基盤、「だから何ができるか」からは各サービス。この一線を引いたことで、課金を集約しても、基盤が全サービスの機能仕様を抱え込まずに済んでいます。

ELN Billingの守備範囲 — 基盤が持つもの(Stripe決済・契約の正本・署名付きWebhook)と持たないもの(プランの機能解釈・認証・各サービスの仕様)の責務分担(実装済みの分担から作図)

認証も持たない

もう1つ「持たない」と決めたのが認証です。課金基盤は誰がどのサービスを契約しているかを扱うので、ログインの仕組みを自前で作りたくなります。ここは、先に紹介した社内の認証基盤(ELN ID)へ委譲しました。利用者のIDは全サービスで統一され、課金基盤は認証の結果だけを受け取ります。

委譲すれば楽になる、とだけ書くのは正確ではないので付け加えます。認証を外に出した代わりに、検証環境を1つ増やすたびに、その環境の戻り先URLを認証基盤側にも登録する、という運用作業が残りました。委譲は実装を消しますが、外部との結び付きは増えます。

Stripeは1アカウントに集約する

Stripe側の構成も、選択肢を比べて決めています。サービスごとにStripeアカウントを分ける案は、集約の意味がなくなるので却下。Stripeのマルチアカウント連携機能(Connect)は、手数料構造が変わり運用も複雑になるので却下。採ったのは、1つのStripeアカウントに全サービスの決済を流し、決済ごとの付帯情報(メタデータ)にサービスID・プランID・利用者IDを焼き込んで識別する方式です。Stripeの管理画面1つで全サービスの売上が見え、どの決済がどのサービスのものかは付帯情報で機械的に振り分けられます。

Stripeから届くイベントの二重処理も基盤側で防ぎます。イベントIDを7日間の期限付きでデータベースに記録し、同じイベントが再送されてきても安全に読み飛ばします。決済イベントは「たまに同じものが2回届く」前提で作るのが正解です。

通知の作り込み — 署名・再送・巻き込まない

各サービスへの通知は、事故が起きやすい境界なので仕様を細かく固めています。

  • 署名の式を仕様書に1行で明記しています。 「送信時刻と本文をピリオドでつないだ文字列」への署名で、ヘッダに署名・送信時刻・イベント種別の3つを付けます。式をここまで具体的に書くのは、後述する事故の教訓です。
  • 再送は1秒→5秒→15秒の間隔で最大3回。 受信側の一時的な障害(5xx)だけを再送し、受信側の実装間違い(4xx)は再送しません。1回の試行は10秒で打ち切ります。
  • 通知の失敗が決済処理を妨げません。 通知がすべて失敗しても、Stripeへの応答は正常に返します。通知の不調で決済そのものが再送ループに入る事態を避けるためです。

webhookが届かない — 5連戦の記録

この基盤で一番学びが多かったのは、2026年4月の「通知が届かない」5連戦です。時系列で書きます。

1戦目(署名形式の不一致)。 送信側は本文だけに署名し、受信側は「送信時刻+本文」への署名を期待していて、全件が改ざん扱いで拒否されていました。根因は仕様書に署名の式が書かれておらず、両側が独自に解釈していたこと。このとき署名式を仕様書に明記し、送受で一致することを契約テストにしました。

2戦目(3層の連鎖設定不備)。 署名を直しても届きません。誤った仮説を3回経た後にたどり着いた真因は、設定の3連鎖でした。Stripeに通知先が1件も登録されていない。配信環境に検証用の秘密値が設定されていない。データベース上の通知先URLが http://localhost:3000 のまま。どれか1つでも欠けると全体が沈黙します。対策として、localhost系URLをAPIで拒否し、3層を1コマンドで検証するスクリプトを作り、本番設定のチェックリストを整備しました。

3戦目(投げ捨てた非同期処理)。 まだ届かないケースが残ります。今度は送信処理が「投げっぱなし」(結果を待たずに処理を終える書き方)になっていて、サーバーレス実行環境が処理終了と同時に実行中の送信を凍結・中断していました。送信を全箇所で待つ形に直し、「処理が終わる時点で送信が完了済みである」ことを検査する回帰テストを追加しました。サーバーレスでは投げっぱなしは壊れます。

4戦目(受信側の設定と壊れたビルド)。 それでも届かない最後の1件は、受信側サービスの秘密値が未設定で、しかもその受信側はビルドパイプラインが2日間壊れていて設定を追加しても反映されない状態でした。このとき決めたのが「受信側の応答本文を最初に見る」「実機の通し検証は、すべての仮説に優先する」です。以後、通知の配送を実際に流して確かめるスクリプトをデプロイの必須ゲートにしています。

推測で直しては外す、を4回繰り返して学んだことは1つです。境界をまたぐ機能は、両端の実物を見るまで直ったと言わない。

つなぎ方 — 各サービスから見た導入手順

新しいSaaSがこの基盤に乗るときの手順は、型が決まっています。

  1. サービスを登録してAPIキーを受け取ります。 キーはデータベースに元の値では残らず、変換値だけが保存されます。通知検証用の秘密値も、登録時に一度だけ返る方式です。
  2. プランを自分のAPIキーで登録します。 価格・請求サイクル・試用期間は基盤側の属性、機能の中身は前述の自由記述欄に書きます。
  3. 決済への入口を置きます。 自サービスの画面に「契約する」ボタンを置き、押されたらプランIDと戻り先URLを基盤へ渡してStripeの決済ページへ転送します。決済ページの見た目や入力の安全性はStripeが担保します。
  4. 通知を受けて契約状態を反映します。 契約成立・解約・支払い失敗の通知を受け取り、署名を検証して自サービスの利用可否を切り替えます。
  5. 画面表示や保護対象の入口では、契約状態を照会します。 「この利用者はいま有効に契約しているか」を1本のAPIで聞けます。

クーポンの運用も基盤側でまとまっています。割引率か固定額、有効期間、全体の利用回数、1人あたりの回数、対象プラン、対象サービスを設定でき、管理画面で作るとStripe側のクーポンにも自動で同期されます。割引が100%になる場合はカード入力そのものを省略できます。無料キャンペーンでカード番号を求めて離脱される、という定番の取りこぼしへの対応です。

クレジット(前払いの利用枠)には、購入分と特典分の区別があります。特典分には有効期限があり、残高を読むときに期限切れを清算する方式にしました。期限切れを消すためだけの定期実行を持たない、という運用コストの判断です。

お金を扱うコードの規律

課金基盤には「だいたい合っている」がありません。2026年9月には、決済に関わる経路を集中的に洗い直し、16件の不具合を修正して、7つの決済経路すべてを実機で通し検証しました。代表的な直しを2つ挙げます。

  • 紹介報酬の「ちょうど1回」保証。 紹介の報酬付与が、同時アクセスで二重に付き、一時的な障害では逆に永久に失われる作りになっていました。データベースの条件付き書き込み(すでに記録があれば失敗する書き込み)で「付与の権利を先に1件だけ確保する」方式に直し、実データベースを使った統合テストで並行実行を検証しています。
  • クーポンのサービス境界。 あるサービスのクーポンが、別のサービスの決済でも有効になっていました。応答をAPIキーのサービスに絞る大原則の適用漏れです。境界の検証は「全APIで」が原則で、1本でも漏れれば事故になります。

開発の裏側 — 初日に土台、6か月で32,000行

このリポジトリの最初のコミットは2026年3月1日です。その初日のうちに、認証連携・データベース・Stripe連携・決済ページ・Webhook処理・契約管理・管理画面・売上分析・署名付き通知・配信設定までの土台を19コミットで一気に組みました。AIとの開発では、最初の1日で「動く全体」を作り切って、以後は実際の利用で見つかる問題を潰していく進め方が合っています。

以後6か月でコミット172件。実装はTypeScript 233ファイル・約32,000行。仕様書17本、テスト258件(うち実データベースを使う統合テスト9ファイル)、ブラウザ経由の通し検証4本。決済という性質上、テストの厚みは他の製品より意図的に厚くしています。

置き場所は独立リポジトリです。コーポレートサイトに同居させる案も共通リポジトリに入れる案もありましたが、「課金は壊れたら全SaaSが止まる。ほかの変更に巻き込まれる場所に置かない」を優先しました。通知基盤notifyが共通リポジトリ入りを選んだのと逆の判断で、同じ「基盤」でも壊れたときの影響範囲で置き場所は変わります。

いまの状態

2026年9月にpre-GA版(v0.9)を本番リリースし、最初のSaaSが実際の課金をこの基盤経由で回しています。2つ目のサービスの接続が進行中で、登録予定は計6サービス。正式版(v1.0)のスコープも8機能で確定済みです。利用者向け画面と運営の管理画面は、別ドメイン・別配信に分離して稼働しています。

転用できる教訓

  • 集約サービスは「何を持たないか」を先に決めます。データの正本は持ち、データの解釈は利用側に残すと、基盤が各サービスの変更に巻き込まれません。禁止事項は文書に明文化します。
  • 決済のような「壊れたら全部止まる」機能は、他の変更に巻き込まれない独立した置き場所に隔離します。依存の向きは常に「サービス→基盤」の一方向に固定します。
  • 通知には署名と自動再送を最初から入れ、署名の式そのものを仕様書に書きます。送信側と受信側が式を独自解釈した時点で全件が止まります。
  • 境界をまたぐ機能の障害は、推測で直さず両端の実物を見ます。設定は1か所ではなく連鎖で欠けるので、全層を1コマンドで検証できるようにして、デプロイの必須ゲートにします。
  • お金の増減に関わる処理は「ちょうど1回」をデータベースの条件付き書き込みで保証し、並行実行を実データベースの統合テストで検証します。

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

ここで紹介した基盤の上で動く自社プロダクトは 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 をご覧ください。