BluePeriod Docs
開発

Stripe 実装アーキテクチャ

Stripeサブスクリプション実装のアーキテクチャとデータフロー

15a. Stripe 実装アーキテクチャ

1. 概要

本ドキュメントは、BluePeriod の Stripe サブスクリプション実装のアーキテクチャ・データフロー・コンポーネント構造を定義する永続的な仕様書(System Document)である。開発者向けの操作ガイド・トラブルシューティングは 15_stripe_integration_guide を参照のこと。


2. コンポーネント構造

2.1. レイヤー分離

┌─────────────────────────────────────────────────────────┐
│  UI Layer                                               │
│  PricingPage / BillingSettings / SuccessPage            │
│  ↕ paymentAdapter (adapter.ts)                          │
├─────────────────────────────────────────────────────────┤
│  API Layer (Hono)                                       │
│  POST /stripe/checkout                                  │
│  POST /stripe/portal                                    │
│  GET  /stripe/verify-session                            │
│  POST /stripe/webhook                                   │
├─────────────────────────────────────────────────────────┤
│  Service Layer                                          │
│  service.ts  (Checkout/Portal セッション生成)            │
│  admin-sync.ts  (DB同期コアロジック)                     │
├─────────────────────────────────────────────────────────┤
│  Infrastructure                                         │
│  stripe.ts  (Stripeクライアント初期化)                   │
│  supabase/admin.ts  (Admin権限Supabaseクライアント)       │
└─────────────────────────────────────────────────────────┘

2.2. 各ファイルの責務

ファイルパス責務依存
stripe.tssrc/lib/stripe.tsStripe SDKの初期化・シングルトン提供stripe npm
service.tssrc/lib/stripe/service.tsCheckout/Portalセッションの生成ロジックstripe.ts, admin-sync.ts
adapter.tssrc/lib/stripe/adapter.tsフロントエンド用fetchラッパーなし(fetch APIのみ)
admin-sync.tssrc/lib/stripe/admin-sync.tsWebhook経由のDB同期・顧客管理stripe.ts, supabase/admin.ts
stripe.ts (API)src/server/api/stripe.tsHTTPルーティング・認証・バリデーションservice.ts, admin-sync.ts
useSubscription.tssrc/hooks/useSubscription.tsReactフック(購読状態 + Realtime)supabase/client.ts

3. データフロー

3.1. Checkout フロー

[User] ─①─→ PricingPage
         │  ② checkoutボタン押下

      paymentAdapter.checkout(priceId, trialDays)
         │  ③ POST /api/stripe/checkout

      [API] 認証 → is_pro確認 → trial確認 → Price ID検証
         │  ④ createStripeCheckoutSession()

      [Stripe] セッション生成 → URL返却
         │  ⑤ window.location.assign(url)

      [Stripe Hosted Checkout] ユーザーがカード入力
         │  ⑥ 決済完了

      /checkout/success?session_id=xxx
         │  ⑦ GET /api/stripe/verify-session

      [API] Stripe APIでセッション検証 → 結果返却

3.2. Webhook 同期フロー

[Stripe] ─①─→ POST /api/stripe/webhook
              │  ② constructEventAsync で署名検証

           イベントタイプ分岐

    ┌─────────┼──────────────┐
    ↓         ↓              ↓
checkout.  subscription.*  invoice.
session.   (created/       payment_
completed  updated/deleted)  succeeded
    │         │              │
    ↓         ↓              ↓
createCustomer   upsertSubscriptionRecord
Mapping           (共通処理)
    │                    │
    └──────┬─────────────┘

    subscriptions テーブル upsert

           ↓(DBトリガー自動発火)
    ┌──────┼──────────┐
    ↓                 ↓
handle_pro_status   handle_trial_usage
_update()           _update()
    │                 │
    ↓                 ↓
profiles.is_pro    profiles.has_used_trial
    │                 │
    └────────┬────────┘
             ↓(Supabase Realtime)
    useSubscription フックが変更を検知
    → フロントエンドのUIを即時更新

3.3. Customer Portal フロー

[User] → BillingSettings → paymentAdapter.openPortal()
         │  POST /api/stripe/portal

      [API] 認証 → getOrCreateCustomer → セッション生成


      [Stripe Hosted Portal] 支払い方法変更・キャンセル等


      /settings(戻り先)

4. データモデル

4.1. Stripe 側オブジェクト

オブジェクト用途識別子
CustomerSupabaseユーザーとStripe顧客の紐付けcus_...
Priceプランの価格定義price_...
Subscription定期購読の状態管理sub_...
Checkout Session決済フローのセッションcs_...
Billing Portal Session顧客ポータルのセッションbps_...

4.2. Supabase 側テーブル

customers

カラム説明
idUUID (PK)Supabase auth.users.id と同一
stripe_customer_idTEXT (UNIQUE)Stripe Customer ID

subscriptions

カラム説明
idTEXT (PK)Stripe Subscription ID
user_idUUID (FK)customers.id
statusTEXTtrialing / active / past_due / unpaid / canceled / incomplete / incomplete_expired / paused
price_idTEXTStripe Price ID
metadataJSONBStripe Metadata のスナップショット
current_period_startTIMESTAMPTZ現在期間の開始日
current_period_endTIMESTAMPTZ現在期間の終了日
trial_startTIMESTAMPTZトライアル開始日
trial_endTIMESTAMPTZトライアル終了日
cancel_at_period_endBOOLEAN期末キャンセル予定か
createdTIMESTAMPTZ作成日

profiles (関連カラム)

カラム説明
is_proBOOLEANPro会員フラグ(キャッシュ)
has_used_trialBOOLEANトライアル使用済みフラグ(一方向)

4.3. オブジェクト間のマッピング

auth.users.id ──── customers.id ──── customers.stripe_customer_id
      │                                       │
      │                                       ↓
      └──── profiles.is_pro         Stripe Customer
      └──── profiles.has_used_trial        │

                                 subscriptions.user_id


                                 Stripe Subscription

4.4. Single Source of Truth

データ主たる更新元補足
subscriptions テーブルadmin-sync.ts 経由の upsertWebhookイベントから同期
profiles.is_proDBトリガー handle_pro_status_update()subscriptions テーブルの変更が起点
profiles.has_used_trialDBトリガー handle_trial_usage_update()一方向(trueにしか遷移しない)
customers マッピングcreateCustomerMapping()checkout.session.completed イベントで作成

5. セキュリティ設計

5.1. 多層防御

防御対象実装
API認証未認証ユーザーsupabase.auth.getUser()
Price ID検証不正な価格での購読環境変数との一致チェック
二重購買防止既Proユーザーの再購読profiles.is_pro チェック
トライアル防止トライアルの無限利用API + DBトリガー + UI の3層
Webhook署名偽装イベントconstructEventAsync
RLSテーブルアクセス制御auth.uid() ベース
RuntimeNode.js(Edge Runtimeは使用しない)バックエンド方針に準拠

5.2. 環境変数

変数用途必須
STRIPE_SECRET_KEYStripe API認証
STRIPE_WEBHOOK_SECRETWebhook署名検証
NEXT_PUBLIC_STRIPE_PRO_PRICE_IDPro価格ID(UI + API検証)
SUPABASE_SERVICE_ROLE_KEYWebhook処理のAdmin操作
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY未使用(ホスト型Checkoutのため不要)

6. DBトリガー定義

6.1. handle_pro_status_update()

  • 対象: subscriptions テーブルの INSERT / UPDATE / DELETE
  • 動作: statusactive または trialing の場合に profiles.is_pro = true、それ以外は false
  • 設定: migration 20260215160500

6.2. handle_trial_usage_update()

  • 対象: subscriptions テーブルの INSERT / UPDATE
  • 動作: trialing または trial_start IS NOT NULL の場合に profiles.has_used_trial = true
  • 冪等性: AND has_used_trial = false で無駄な更新を防止
  • 一方向性: 一度 true になったら false に戻らない
  • 設定: migration 20260216184500

6.3. is_pro_user() RPC関数

  • 動作: profiles.is_pro を参照してPro判定を返す
  • 用途: RLSポリシー、API認可、UI表示
  • セキュリティ: security definer でRLSをバイパス

7. Supabase Realtime 設計

7.1. useSubscription フック

  • チャンネル: subscription-changes
  • 監視対象: public.subscriptions テーブルの全イベント
  • フィルタ: user_id = eq.${userId}
  • ハンドリング:
    • INSERT/UPDATE: ステータスが trialing/active/past_due/unpaid なら更新
    • DELETE: subscriptionnull に設定
  • ライフサイクル: コンポーネントのアンマウント時にチャンネルを removeChannel()

関連ドキュメント:

On this page