FLARES LLC
FLARES LLC

Technical document

B2B前提のマルチテナント基盤に、B2Cサブスクリプションを1テナントとして同居させる設計

既存のB2B向けマルチテナント基盤は、組織に招待されたユーザーが管理者の承認を経て使い始める、という前提で認証が設計されています。ここに、招待なしで即座に使い始められる一般消費者向けのサブスクリプションサービスを追加する場合、既存の認証モデルをテナントごとに作り変えるわけにはいきません。ここでは、基盤全体の認証モデルを崩さずに、例外的な1テナントだけ挙動を変えるために採用した設計を、意思決定の経緯とともに整理します。
アーキテクチャ / マルチテナント設計約22分公開日 2026年7月12日更新日 2026年7月12日
B2B前提のマルチテナント基盤に、B2Cサブスクリプションを1テナントとして同居させる設計のアイキャッチ

Summary

この文書の要点

  • 共有認証の招待前提を壊さず、対象テナントだけに自動アクティベートを許可する認証realmを切り出します。
  • 長時間かかる生成処理は、永続ジョブ+境界つきステップAPIで、途中失敗しても自動的に再開できる形にします。
  • 専用のキューサービスを持たず、DBの行カウントでインスタンスをまたぐ同時実行数を制御します。
  • 見積りと本課金を分けた二段階決済を、冪等性キーで安全に再試行できるようにします。

招待制の基盤に、招待なしで使えるサービスを追加する要求

既存のマルチテナント業務SaaS基盤は、勤怠管理や販売管理のような、組織単位で契約し、組織の管理者が利用者を招待して使い始める業務システム群を束ねています。この前提では、新規ユーザーは招待されるまで「保留」状態であり、組織側の管理者が有効化して初めて利用できます。組織という単位が存在し、利用者は組織に所属してから使い始める、という順序が固定されています。

ここに、学習トピックを入力するとAIがコンテンツ化して届ける、一般消費者向けのサブスクリプションサービスを追加する要求が生まれました。この種のサービスには「組織」という概念がそもそも存在せず、利用者はGoogleアカウントでログインした瞬間から使い始められることが期待されます。既存の招待前提の認証モデルをそのまま適用すると、サービスとして成立しません。

選択肢は2つありました。ひとつは、共有認証基盤そのものに「組織なしで即時利用できるユーザー種別」を追加する改修。もうひとつは、既存の共有認証には触れず、この1テナントだけに例外的な挙動を許す仕組みを別途用意することです。共有認証への改修は影響範囲が全テナントに及ぶため、後者を選びました。

共有認証を変えずに、例外を局所化するという制約

共有認証基盤は、他の多数のB2Bテナントが依存している基盤コンポーネントです。ここに「このテナントだけは自動アクティベートする」という条件分岐をアプリケーションコードの中に書き込むと、条件分岐の見落としひとつが他のテナントの認証挙動に影響しかねません。共有基盤のコードは変えず、設定やインフラの層で例外を表現する、という制約を最初に置きました。

同時に、この消費者向けサービスは共有のシングルサインオン(SSO)のCookieスコープに乗せるべきではない、という判断もしました。共有SSOのセッションCookieが、招待制の業務システム群と、招待なしの消費者向けサービスの両方に有効なスコープを持つと、片方のセッション事故がもう片方に波及するリスクを抱え込みます。

技術スタック

バックエンドはBun+Honoで構築し、決済にStripe、コンテンツ生成には複数系統の生成AI API(テキスト生成と音声/メディア生成)を使っています。フロントエンドはReact Router 8によるSPAで、当初はElectronによるデスクトップアプリとExpoによるモバイルアプリのシェルも用意しましたが、後述の理由でPWAを主軸に据える方針へ転換しました。

認証realmの分離という逃げ道

採用した設計は、共有認証基盤の中に、このテナント専用の「認証realm」を新設し、その中でだけ「初回ログイン時に自動でアクティベートする」設定を有効にするというものです。この設定はアプリケーションのロジックではなく、インフラのプロビジョニング定義(IaC)の中で、この特定のrealmにのみスコープされたフラグとして表現しています。共有認証のコード自体には一切手を入れません。

さらに、このrealmは専用の認証バックエンドと専用のCookieドメインで動作させ、既存のB2Bテナント群が使う共有SSOのCookieスコープの外に完全に切り離しました。ドメインレベルでスコープを分けているため、ブラウザの同一オリジンポリシーの働きにより、消費者向けサービスのセッションCookieが業務システム群に送られることも、その逆も、構造的に起こり得ません。

この設計のポイントは、「例外を許すコードを書く」のではなく「例外が構造的にこの範囲でしか成立しないようにインフラで区切る」ことです。アプリケーションコードのif文で例外を表現すると、将来別の開発者がその分岐の意味を理解しないまま触ってしまうリスクが残ります。インフラの境界として表現しておけば、境界を壊すには明示的にインフラ定義を変更する必要があり、事故が起きにくくなります。

  • 自動アクティベートのフラグは、アプリケーションコードではなくIaC上でこのrealmにのみスコープする。
  • 専用の認証バックエンド・専用Cookieドメインで、共有SSOのスコープから完全に切り離す。
  • 「例外を許すコード」ではなく「例外しか成立しないインフラの境界」として表現する。

永続ジョブ+境界つきステップAPIのワーカーパターン

学習トピックからコンテンツを生成する処理は、複数の外部AI APIを順番に呼び出す、数十秒から数分かかる非同期処理です。Cloud Runのような、リクエストごとに実行時間の上限があるサーバーレス環境では、1回のHTTPリクエストの中で生成処理全体を完結させることはできません。

そこで、生成処理全体を「ジョブ」としてDBに永続化し、ジョブの各ステップ(トピック展開、素材生成、後処理など)を、それぞれ独立した境界つきのHTTP呼び出しとして実行する設計にしました。1回のHTTPリクエストは1ステップ分だけを進め、完了したらジョブの状態を更新して終了します。次のステップは、別のHTTPリクエスト(内部ワーカーからの呼び出し)としてトリガーされます。

ジョブの排他制御には、行ロックを使ったスキップ方式(対象行がロックされていれば待たずに次の候補へ進む方式)を採用し、複数のワーカーインスタンスが同じジョブを二重に処理しないようにしています。加えて、各ステップの開始時にハートビート(生存確認のタイムスタンプ)を記録し、一定時間ハートビートが更新されないジョブは「止まった」とみなして自動的に再取得・再開できるようにしました。これにより、途中のインスタンスがクラッシュしても、ジョブが永久に止まったままにならず、人手を介さず自己修復します。

  • 生成処理全体を永続ジョブとしてDBに持ち、各ステップを境界つきHTTP呼び出しに分解する。
  • 行ロックのスキップ方式で、複数ワーカーによる二重処理を防ぐ。
  • ハートビートによる停止ジョブの自動検知と再取得で、人手を介さず自己修復する。

DBカウント方式のグローバル同時実行制御

コンテンツ生成の中で最も費用と負荷が大きいステップ(メディア生成API呼び出し)には、複数のCloud Runインスタンスをまたいだグローバルな同時実行数の上限を設ける必要がありました。専用のキューサービスやメッセージブローカーを別途構築する選択肢もありましたが、この規模ではその運用コストに見合わないと判断し、より軽量な代替策を採用しました。

採用したのは、「現在実行中」の状態を持つジョブの件数をDBに問い合わせ、上限に達していれば新規実行を待たせる、という単純なカウント方式です。インスタンスをまたいだ状態共有はデータベースというすでに存在する共有リソースに任せ、新しいミドルウェアを増やしませんでした。トランザクションによる整合性はDB自体が保証してくれるため、実装も比較的単純です。

この方式は、真の意味での公平なキューイングやスループット保証はできませんが、「同時に走らせすぎて外部APIのレート制限に触れる、あるいは費用が急騰する」という最も避けたい事態を防ぐには十分でした。専用のキューサービスを持たないことのトレードオフを理解した上で、規模に見合った軽量な解決策を選んだ、という判断です。

二段階Stripe決済の冪等性設計

決済は、見積り(生成にどれくらいの費用がかかるかをユーザーに提示する段階)と、本課金(実際に生成を開始する段階)の2段階に分けています。生成処理はトピックの深さ(軽量・標準・じっくり、のような選択肢)によって費用が変わるため、課金前にユーザーへ見積りを提示し、同意を得てから初めて課金する流れが必要でした。

二段階に分けたことで、それぞれの段階でネットワーク再試行が発生しても二重課金が起きないよう、冪等性キーを使ったリクエスト設計を徹底しています。同じ操作が意図せず複数回送信されても、Stripe側で同一の操作として扱われ、二重に課金されることを防ぎます。決済のような取り消しにくい操作ほど、UIの防止策(二重クリック防止など)だけに頼らず、サーバー側の冪等性で担保することを優先しました。

JST基準の日次/月次レポートを運用チャットへ自動投稿する

売上、返金、外部API費用、粗利益といった運用上重要な指標は、日本時間(JST)を基準とした日次バケットで集計し、毎朝決まった時刻に運用チャットへ自動投稿しています。月初には月次サマリーも合わせて投稿します。ダッシュボードを都度開いて確認する運用ではなく、必要な数字が向こうから届く形にすることで、異常があった時に気づくまでの時間を短くする狙いです。

この手のレポートは複数の通貨・複数の課金経路にまたがることがあるため、集計ロジックをレポート生成の内部に閉じ込め、日次バッチの内部ワーカーAPIとして実装しています。認証済みの内部呼び出しのみを受け付ける経路にすることで、外部から任意に叩けるエンドポイントにはしていません。

配信の有効期限とデータの保持期間を分離する

生成したコンテンツはGCSなどのオブジェクトストレージに保存し、利用者への配信には署名付きURLを使っています。一定の保持期間(例えば1年)を過ぎると、署名付きURLの発行自体を止め、アクセスすると410(Gone)を返す設計にしましたが、DB上のレコード自体は監査目的で削除せずに残しています。

「利用者がアクセスできる期間」と「事業者側が記録として保持する期間」は、目的が異なるため意図的に分離しました。前者を過ぎたら後者も一緒に消してしまうと、後から問い合わせやトラブル対応が発生した際に、何が起きたかを追跡する手段を失います。ストレージコストと監査要件のバランスを取る設計として、配信の停止とデータの削除を同じタイミングにしない、という判断です。

ネイティブアプリ配布を見送り、PWAへ寄せた判断

当初はElectronによるデスクトップアプリと、Expoによるモバイルアプリのシェルも用意していましたが、開発が進む中でPWA(プログレッシブWebアプリ)を主軸にする方針へ転換しました。理由のひとつは、Googleログインを使う認証フローが、Electronのように組み込みブラウザ(WebView)内で実行される環境からのOAuth認証を、セキュリティ上の理由でブロックするポリシーに抵触したことです。

この制限を回避する技術的な手段(外部ブラウザを別途起動して認証を完結させる、など)は存在しますが、実装と保守の複雑さが増します。加えて、ネイティブアプリとしてストアに配布する場合、審査プロセスやアップデート配布の運用コストが継続的に発生します。ブラウザだけで完結し、インストール不要でアクセスできるPWAであれば、この種の制約や運用コストを避けられます。

小規模なチームで立ち上げたサービスにとって、ネイティブアプリ特有の運用コストは無視できない負担です。将来的にネイティブアプリならではの体験(プッシュ通知やオフライン再生の質など)が事業上どうしても必要になった時点で再検討する、という前提のもと、初期段階ではPWA優先に舵を切りました。

検証観点: realm分離の影響確認と同時実行上限の負荷試験

認証realmを新設する際は、既存のB2Bテナント側のログイン・セッション挙動に一切影響が出ないことを最優先で確認しました。具体的には、既存テナントのCookieが新設realmのドメインに送信されないこと、新設realm側のCookieが既存の共有SSOドメインに漏れないことを、実際のブラウザ挙動で確認しています。

グローバル同時実行制御については、上限に達している状態で新規リクエストが正しく待たされること、待機中のリクエストが上限解放後に処理されること、複数インスタンスを同時に起動した状態でも合計の同時実行数が上限を超えないことを、負荷試験で確認しました。DBカウント方式は実装が単純な分、境界値でのレースコンディションが起きやすいため、この検証は重点的に行っています。

この設計が向かない条件

この設計は、既存の共有基盤の認証モデルを変えられない、かつ例外的なテナントの数が少数に限られる場合に有効です。同じような例外を持つテナントが増えてくると、realmごとに個別対応するコストが積み上がり、どこかの時点で「共有認証基盤そのものにユーザー種別という概念を正式に追加する」改修に踏み切ったほうが安くなります。局所的な例外処理は、例外が少数である間だけ有効な近道です。

また、DBカウント方式のグローバル同時実行制御は、呼び出し頻度や規模がさらに大きくなった場合には、専用のキューサービスへの移行を検討すべき領域です。今回の規模ではデータベースへの追加負荷が許容範囲でしたが、スケールが変われば前提も変わります。

まとめ: 既存の認証モデルを壊さずに、例外的なテナントだけ挙動を変える

この題材で一貫していたのは、「既存の多数のテナントが依存する共有の仕組みは変えず、例外を局所化する」という判断でした。認証realmの分離、専用ワーカーパターン、DBカウント方式の同時実行制御、いずれも、共有基盤そのものに手を入れるコストとリスクを避け、対象を1テナントに閉じ込める形で実現しています。

新しい種類のテナントを既存のマルチテナント基盤に追加するときに問われるのは、機能をどう実装するかよりも先に、既存の前提(この場合は「ユーザーは組織に招待される」)をどこまで守り、どこを局所的に破ってよいかという線引きです。この線引きを曖昧にしたまま実装を進めると、共有基盤の複雑さが際限なく増えていきます。

Technical documents

技術文書を増やしていきます。

AI、クラウド、業務アプリ開発、要件定義、運用設計に関する考え方を、今後も文書として整理します。

技術文書一覧へ