開発
Stripe 実装アーキテクチャ
Stripeサブスクリプション実装のアーキテクチャとデータフロー
本ドキュメントは、BluePeriod の Stripe サブスクリプション実装のアーキテクチャ・データフロー・コンポーネント構造を定義する永続的な仕様書(System Document)である。開発者向けの操作ガイド・トラブルシューティングは 15_stripe_integration_guide を参照のこと。
┌─────────────────────────────────────────────────────────┐
│ 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クライアント) │
└─────────────────────────────────────────────────────────┘
| ファイル | パス | 責務 | 依存 |
|---|
stripe.ts | src/lib/stripe.ts | Stripe SDKの初期化・シングルトン提供 | stripe npm |
service.ts | src/lib/stripe/service.ts | Checkout/Portalセッションの生成ロジック | stripe.ts, admin-sync.ts |
adapter.ts | src/lib/stripe/adapter.ts | フロントエンド用fetchラッパー | なし(fetch APIのみ) |
admin-sync.ts | src/lib/stripe/admin-sync.ts | Webhook経由のDB同期・顧客管理 | stripe.ts, supabase/admin.ts |
stripe.ts (API) | src/server/api/stripe.ts | HTTPルーティング・認証・バリデーション | service.ts, admin-sync.ts |
useSubscription.ts | src/hooks/useSubscription.ts | Reactフック(購読状態 + Realtime) | supabase/client.ts |
[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でセッション検証 → 結果返却
[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を即時更新
[User] → BillingSettings → paymentAdapter.openPortal()
│ POST /api/stripe/portal
↓
[API] 認証 → getOrCreateCustomer → セッション生成
│
↓
[Stripe Hosted Portal] 支払い方法変更・キャンセル等
│
↓
/settings(戻り先)
| オブジェクト | 用途 | 識別子 |
|---|
| Customer | SupabaseユーザーとStripe顧客の紐付け | cus_... |
| Price | プランの価格定義 | price_... |
| Subscription | 定期購読の状態管理 | sub_... |
| Checkout Session | 決済フローのセッション | cs_... |
| Billing Portal Session | 顧客ポータルのセッション | bps_... |
| カラム | 型 | 説明 |
|---|
id | UUID (PK) | Supabase auth.users.id と同一 |
stripe_customer_id | TEXT (UNIQUE) | Stripe Customer ID |
| カラム | 型 | 説明 |
|---|
id | TEXT (PK) | Stripe Subscription ID |
user_id | UUID (FK) | customers.id |
status | TEXT | trialing / active / past_due / unpaid / canceled / incomplete / incomplete_expired / paused |
price_id | TEXT | Stripe Price ID |
metadata | JSONB | Stripe Metadata のスナップショット |
current_period_start | TIMESTAMPTZ | 現在期間の開始日 |
current_period_end | TIMESTAMPTZ | 現在期間の終了日 |
trial_start | TIMESTAMPTZ | トライアル開始日 |
trial_end | TIMESTAMPTZ | トライアル終了日 |
cancel_at_period_end | BOOLEAN | 期末キャンセル予定か |
created | TIMESTAMPTZ | 作成日 |
| カラム | 型 | 説明 |
|---|
is_pro | BOOLEAN | Pro会員フラグ(キャッシュ) |
has_used_trial | BOOLEAN | トライアル使用済みフラグ(一方向) |
auth.users.id ──── customers.id ──── customers.stripe_customer_id
│ │
│ ↓
└──── profiles.is_pro Stripe Customer
└──── profiles.has_used_trial │
↓
subscriptions.user_id
│
↓
Stripe Subscription
| データ | 主たる更新元 | 補足 |
|---|
subscriptions テーブル | admin-sync.ts 経由の upsert | Webhookイベントから同期 |
profiles.is_pro | DBトリガー handle_pro_status_update() | subscriptions テーブルの変更が起点 |
profiles.has_used_trial | DBトリガー handle_trial_usage_update() | 一方向(trueにしか遷移しない) |
customers マッピング | createCustomerMapping() | checkout.session.completed イベントで作成 |
| 層 | 防御対象 | 実装 |
|---|
| API認証 | 未認証ユーザー | supabase.auth.getUser() |
| Price ID検証 | 不正な価格での購読 | 環境変数との一致チェック |
| 二重購買防止 | 既Proユーザーの再購読 | profiles.is_pro チェック |
| トライアル防止 | トライアルの無限利用 | API + DBトリガー + UI の3層 |
| Webhook署名 | 偽装イベント | constructEventAsync |
| RLS | テーブルアクセス制御 | auth.uid() ベース |
| Runtime | Node.js(Edge Runtimeは使用しない) | バックエンド方針に準拠 |
| 変数 | 用途 | 必須 |
|---|
STRIPE_SECRET_KEY | Stripe API認証 | ✅ |
STRIPE_WEBHOOK_SECRET | Webhook署名検証 | ✅ |
NEXT_PUBLIC_STRIPE_PRO_PRICE_ID | Pro価格ID(UI + API検証) | ✅ |
SUPABASE_SERVICE_ROLE_KEY | Webhook処理のAdmin操作 | ✅ |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY | 未使用(ホスト型Checkoutのため不要) | ❌ |
- 対象:
subscriptions テーブルの INSERT / UPDATE / DELETE
- 動作:
status が active または trialing の場合に profiles.is_pro = true、それ以外は false
- 設定: migration
20260215160500
- 対象:
subscriptions テーブルの INSERT / UPDATE
- 動作:
trialing または trial_start IS NOT NULL の場合に profiles.has_used_trial = true
- 冪等性:
AND has_used_trial = false で無駄な更新を防止
- 一方向性: 一度
true になったら false に戻らない
- 設定: migration
20260216184500
- 動作:
profiles.is_pro を参照してPro判定を返す
- 用途: RLSポリシー、API認可、UI表示
- セキュリティ:
security definer でRLSをバイパス
- チャンネル:
subscription-changes
- 監視対象:
public.subscriptions テーブルの全イベント
- フィルタ:
user_id = eq.${userId}
- ハンドリング:
- INSERT/UPDATE: ステータスが
trialing/active/past_due/unpaid なら更新
- DELETE:
subscription を null に設定
- ライフサイクル: コンポーネントのアンマウント時にチャンネルを
removeChannel()
関連ドキュメント: