BluePeriod Docs
開発

Stripe サブスクリプション統合ガイド

Stripeを用いたProプラン登録と決済機能の統合手順

15. Stripe サブスクリプション統合ガイド

1. 概要

本プロジェクトでは、Stripe を使用して「BluePeriod Pro」サブスクリプションプランを提供しています。ユーザーは Pro プランに加入することで、クラウド同期、無制限の AI 執筆アシスタント、キャラクターチャットなどの高度な機能を利用できます。

2. アーキテクチャ

決済フローは Stripe ホスト型の Checkout API をベースにしており、Supabase DB との同期は Webhook によって行われます。

決済フロー

  1. Checkout セッション作成: アプリ内から /api/stripe/checkout (POST) を呼び出し、Stripe の決済ページへリダイレクトします。
  2. 決済完了: Stripe のページでユーザーが操作を完了すると、/checkout/success へ戻ります。
  3. 非同期同期 (Webhook): Stripe サーバーからアプリの /api/stripe/webhook へ通知が送られ、subscriptions テーブルと profiles テーブルを更新します。

サブスクリプション管理フロー

  • カスタマーポータル: /api/stripe/portal (POST) を呼び出すと、Stripe ホスト型のポータルページへリダイレクトします。ユーザーはここで支払い方法の変更、サブスクリプションのキャンセルなどを行えます。
  • ポータルの表示内容は Stripe ダッシュボード → Settings → Billing → Customer portal で設定します。サブスクリプションのキャンセルを許可する場合は、ダッシュボードで「Allow customers to cancel subscriptions」を有効にする必要があります。

3. 重要:Stripe API バージョンと破壊的変更

本プロジェクトは API バージョン 2025-12-15.clover を使用しています。このバージョン(および 2025-03-31.basil 以降)には重要な破壊的変更が含まれています。

current_period_start/end の場所

旧来のバージョンでは Subscription オブジェクトのルートに存在していましたが、最新バージョンでは各 SubscriptionItem ごとに管理されるようになりました。

  • 誤り: subscription.current_period_start (undefined になり、Date変換で RangeError を引き起こす)
  • 正解: subscription.items.data[0].current_period_start

実装時には、必ず SubscriptionItem から期間情報を取得するようにしてください。

4. 無料トライアルの実装

ユーザーの心理的障壁を下げるため、「クレジットカード入力不要の7日間無料トライアル」を導入しています。

  • トライアル期間: 7日間
  • 実装方法: Checkout Session 作成時に subscription_data.trial_period_days: 7 かつ payment_method_collection: 'if_required' を指定。
  • 挙動: トライアル開始時点ではカード情報は不要。期間終了までにカード情報を登録しなかった場合、サブスクリプションは自動的にキャンセルされます。

5. Webhook ハンドアリング

/api/stripe/webhook では以下の主要なイベントを処理しています。

  • checkout.session.completed: 決済完了時(client_reference_id を用いてユーザーを特定)。
  • customer.subscription.created: サブスクリプション作成時。
  • customer.subscription.updated: サブスクリプションの状態変化時。
  • customer.subscription.deleted: サブスクリプションの削除(キャンセル完了)時。
  • invoice.payment_succeeded: 支払成功時(継続確認)。

【重要】Stripe ダッシュボードの Webhook 設定

上記 5種類すべて を Stripe ダッシュボードの Webhook エンドポイントに登録してください。いずれかが欠けていると、対応するイベントがサーバーに配信されず、DBが更新されません。

設定場所: Developers → Webhooks → エンドポイント編集 → Events

【参考】constructEventAsync の使用

Node.js ランタイム環境では constructEventconstructEventAsync の両方が利用可能ですが、本プロジェクトでは一貫して constructEventAsync を使用しています(Edge Runtime 時代の名残)。動作上の問題はありません。

  • 使用: await stripe.webhooks.constructEventAsync(body, sig, webhookSecret)

同期ロジック (src/lib/stripe/admin-sync.ts)

サブスクリプションの情報が subscriptions テーブルに upsert されると、DB トリガーが自動的に profiles テーブルの is_pro / has_used_trial カラムを更新します。

  • トリガー on_subscription_changehandle_pro_status_update(): status'active' または 'trialing' の場合に is_pro = true、それ以外は is_pro = false
  • トリガー on_subscription_trial_checkhandle_trial_usage_update(): trialing ステータス検知時に has_used_trial = true(一方向、元に戻さない)

設計上の補足:

  • is_pro / has_used_trial の更新は DB トリガーが Single Source of Truth です。アプリコード側では profiles を直接更新しません
  • is_pro はデータベースに永続化される**フラグ(キャッシュ)**です
  • is_pro_user() はこのフラグを参照して判定するRPC関数です
  • フロントエンドやRLSポリシーからは、直接 is_pro カラムを参照するのではなく、is_pro_user() 関数を通じてPro判定を行うことが推奨されます

決済完了後のセッション検証

Success ページ (/checkout/success) では、URL パラメータ session_id を用いて Stripe API でセッション状態を検証します。

  • 検証エンドポイント: GET /api/stripe/verify-session?session_id=xxx
  • session_id なしのアクセスは自動的に /pricing へリダイレクト
  • 検証失敗時はエラー画面を表示(課金済みの場合はサポートへの案内)

リアルタイム更新

useSubscription フックは Supabase Realtime で subscriptions テーブルの変更をリッスンしており、Webhook 到着による DB 更新を即座にフロントエンドに反映します。

Webhook による同期の限界

Webhook は非同期通知であるため、配信が失敗した場合に DB が更新されない可能性があります(ネットワーク障害、サーバーダウン時など)。Stripe は最大3日間リトライしますが、それでも失敗した場合、DBの is_pro 状態と Stripe の実際のサブスクリプション状態が乖離します。

この乖離が発生した場合、手動で Supabase の subscriptions テーブルと profiles テーブルを修正する必要があります。

6. 開発とテスト

CLI とダッシュボード Webhook の違い(重要)

Stripe の Webhook 配信には 2つの経路 があり、これらを混同しないことが重要です。

Stripe CLI (stripe listen)ダッシュボード Webhook
配信先localhost のみダッシュボードで設定した URL
稼働条件ターミナルでプロセスが動いている間のみ常時(24時間365日)
用途ローカル開発中のみ本番環境・デプロイ済みテスト
SecretCLI 起動時に発行される一時的な whsec_...ダッシュボードで確認できる固定の whsec_...

注意: stripe listen で実行したテスト購読のデータが本番の Supabase に書き込まれると、CLI 停止後のイベント(トライアル終了や更新)が DB に反映されず、不整合が発生します。ローカルでテストする場合は、ローカルの Supabase を使用してください。

ローカル開発 (Stripe CLI)

Stripe からの Webhook をローカル PC で受け取れるようにするため、Stripe CLI が必須です。

# Webhook の転送を開始
stripe listen --forward-to localhost:3703/api/stripe/webhook
  1. 出力された webhook signing secret (whsec_...).env.localSTRIPE_WEBHOOK_SECRET に設定します。
  2. Stripe 管理画面で設定されている API バージョンと、ローカルのライブラリ(stripe npm package)のバージョンが一致していることを確認してください。

サンドボックス環境

テスト用の鍵 (pk_test_..., sk_test_...) を使用している間は、Stripe の「テストカード」を使用して決済をシミュレートできます。

stripe trigger コマンドについて

stripe trigger は汎用的なダミーデータを生成するコマンドです。実際の Checkout フロー(mode: "subscription" など)を再現しないため、Webhook の接続確認には使えますが、サブスクリプション処理のテストには不適切です。

サブスクリプション処理のテストを行う場合は、デプロイ済みのサイト(.blue)上でテストカードを使用して実際に購読するのが最も確実です。

ローカル環境で必要な環境変数

ローカルで Webhook 処理をテストする場合、.env.local に以下の環境変数が必要です(.gitignore で管理対象外であることを確認):

STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PRO_PRICE_ID=price_...
NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbG...
SUPABASE_SERVICE_ROLE_KEY=eyJhbG...  # Webhook処理で必須(管理者権限)

7. トラブルシューティング

  • RangeError: Invalid time value: Webhook 処理中に発生した場合、Stripe オブジェクトのプロパティ参照(特に日付フィールド)が API バージョンと食い違っている可能性が非常に高いです。
  • DBに反映されない: Stripe CLI が動いているか、および STRIPE_WEBHOOK_SECRET が正しいかを確認してください。
  • Link 等の決済方法が表示されない: stripe.checkout.sessions.createpayment_method_types を指定せず、Stripe ダッシュボード側で「自動決済方法」を管理するのが現在の推奨です。また、Link は localhost では表示されず、デプロイ済みのドメインが必要です。
  • "Supabase admin environment variables are not set": ローカル環境で Webhook を受信したが、SUPABASE_SERVICE_ROLE_KEY.env.local に設定されていない。同環境変数を追加してください。
  • Webhook 配信が失敗する(Stripe ダッシュボードで確認): Vercel の環境変数(特に STRIPE_WEBHOOK_SECRETSUPABASE_SERVICE_ROLE_KEY)が正しく設定されているか確認。環境変数追加後はリデプロイが必要な場合があります。
  • is_pro が false にならない(DB不整合): Webhook が未到達だった可能性があります。Stripe ダッシュボード → Webhook 配信履歴で該当イベントのステータスを確認し、失敗している場合は手動で Supabase のレコードを修正してください。
  • カスタマーポータルにキャンセルボタンがない: Stripe ダッシュボード → Settings → Billing → Customer portal で「Allow customers to cancel subscriptions」が有効になっているか確認してください。

8. Pro 会員による機能制限 (Feature Gating)

「BluePeriod Pro」の価値を担保するため、特定の機能は profiles.is_pro フラグによって厳格にガードされています。

8.1. UI レベルの表示制限

  • グローバルヘッダー: クラウド同期状態を示すアイコン(SyncStatusIndicator)は、userProfile.is_protrue かつ syncKey が設定されている場合のみ表示されます。
  • バッジ表示: Header および ProfileHeader において、Pro 会員にはセマンティックカラー primary を使用した「PRO」バッジが表示されます。

8.2. 設定画面のガード

  • SyncSettings: クラウド同期の設定項目は、Pro 会員でない場合は「Upgrade to Pro」のプロンプトが表示され、機能へのアクセスが遮断されます。これにより、意図しない設定変更やキーの漏洩を防ぎます。

8.3. バックグラウンド処理のガード

  • AutoSyncManager: クラウドの更新チェック(ブラウザフォーカス時など)は、is_pro フラグが true の場合のみ実行されます。非 Pro ユーザーのブラウザで無駄な API リクエストが発生しないように保護されています。

9. トライアル無限利用の防止 (Trial Prevention Logic)

同一ユーザーが何度も無料トライアルを繰り返す「エクスプロイト」を防ぐため、以下のガードレールが実装されています。

9.1. has_used_trial フラグ

profiles テーブルに has_used_trial (boolean) カラムを設けています。

  • 更新タイミング: DBトリガー handle_trial_usage_update()subscriptions テーブルへの INSERT/UPDATE を検知し、trialing または trial_start IS NOT NULL の場合に true に設定。
  • 永続性: 一度 true になったフラグは、サブスクリプションがキャンセル・終了されても false に戻ることはありません。

9.2. Checkout 時の拒否ロジック

/api/stripe/checkout において、セッションを作成する前にユーザーの has_used_trial をチェックします。

  • has_used_trial === true の場合、クライアントから要求されたトライアル日数に関わらず、trial_period_days を強制的に 0 に設定します。
  • これにより、システムレベルで二度目のトライアル付与を完全に遮断します。

10. 実稼働(Live モード)への移行

本番環境 blueperiod.blue で実際の決済を有効にするための完全な手順です。

10.1. テストモードとライブモードの違い

Stripeダッシュボードの右上トグルで 「テストモード」「ライブモード」 を切り替えます。この2つは完全に独立した環境であり、データは共有されません。

項目テストモードライブモード
APIキーsk_test_... / pk_test_...sk_live_... / pk_live_...
顧客データテスト用顧客のみ実際の顧客
決済テストカードでシミュレーション実際のクレジットカード決済
Webhookテスト用エンドポイント(またはStripe CLI)ライブ用エンドポイント(本番URL)
Price IDprice_1xxx...(テスト用)price_1yyy...(ライブ用、別ID)
Webhook署名シークレットwhsec_...(テスト用)whsec_...(ライブ用、別の値)

重要: テストモードで作成したPrice IDやWebhook署名シークレットは、ライブモードでは使用できません。各モードで独立して作成・設定する必要があります。

10.2. ライブモードの事前準備

10.2.1. Stripeアカウントの本人確認

  • ダッシュボード右上に「本人確認が必要」などの警告が表示されていないか確認
  • 未完了の場合は、Stripeからの入金(ペイアウト)が保留されます
  • 必要書類: 身分証明書、銀行口座情報、事業情報など

10.2.2. 本番用Price(価格)の作成

ライブモードに切り替えてから、Pro プランの価格を作成します。

  1. ダッシュボード右上のトグルを 「ライブモード」 に切り替え
  2. [商品カタログ][商品を追加] をクリック
  3. 以下を入力:
    • 商品名: BluePeriod Pro
    • 説明: BluePeriod Pro サブスクリプション
    • 価格: 月額料金を設定(例: ¥980/月 または $9.99/月)
    • 課金サイクル: 定額月次
  4. 作成完了後、表示される Price ID (price_...) を控える

10.2.3. ドメイン認証

  1. [Settings][Payments][Apple Pay / Google Pay] を開く
  2. blueperiod.blue を登録
  3. ドメインの所有権確認が求められた場合は、DNSレコードまたはファイル配置で認証

10.2.4. カスタマーポータルの設定

  1. [Settings][Billing][Customer portal] を開く
  2. 以下の機能の有効/無効を設定:
    • サブスクリプションのキャンセル: 許可する場合、「Allow customers to cancel subscriptions」を有効化
    • 支払い方法の変更: 原則として許可を推奨
    • 請求書のダウンロード: 許可を推奨
  3. [Save] をクリック

10.3. 本番用Webhookエンドポイントの作成

Webhookとは、Stripe側でイベント(決済完了、サブスクリプション変更など)が発生した際に、StripeサーバーからアプリサーバーへHTTP POSTリクエストを送信する仕組みです。

テスト環境ではStripe CLIがローカルにイベントを転送していましたが、本番ではStripeダッシュボードでエンドポイントURLを登録し、Stripeが直接アプリサーバーにリクエストを送ります。

作成手順

  1. ダッシュボード右上のトグルを 「ライブモード」 に切り替え
  2. [Developers][Webhooks] を開く
  3. [エンドポイントを追加] をクリック
  4. 以下を入力:
    • エンドポイントURL: https://blueperiod.blue/api/stripe/webhook
    • リッスンするイベント: 以下の 5種類をすべて追加
      • checkout.session.completed — 決済完了時
      • customer.subscription.created — サブスクリプション作成時
      • customer.subscription.updated — サブスクリプション更新時
      • customer.subscription.deleted — サブスクリプション削除(キャンセル完了)時
      • invoice.payment_succeeded — 支払い成功時(継続課金の確認)
  5. [エンドポイントを追加] をクリック
  6. 作成完了後、エンドポイントの詳細画面を開く
  7. 「署名シークレット」[表示] をクリックし、whsec_... の値を控える

この whsec_...STRIPE_WEBHOOK_SECRET 環境変数の値になります。 エンドポイントごとに一意であり、テスト用のものとは異なります。

Webhookイベントの配信について

  • Stripeはイベント発生後、最大3日間リトライします(指数バックオフ)
  • ダッシュボードの [Developers] → [Webhooks] → [エンドポイント] から配信履歴と成功/失敗を確認できます
  • 失敗したイベントは [再送] ボタンで手動再送が可能です

テスト用とライブ用のWebhookの共存

テスト用Webhookエンドポイント(https://blueperiod.blue/... をテストモードで登録したもの)とライブ用は独立しています。両方を登録したままでも問題ありません。テストモードのイベントはテスト用エンドポイントに、ライブモードのイベントはライブ用エンドポイントに配信されます。

10.4. Vercel 環境変数の更新

本番環境のVercelにライブモード用の環境変数を設定します。

  1. Vercelダッシュボード → プロジェクト → [Settings][Environment Variables] を開く
  2. Production 環境に対して以下を設定:
環境変数設定する値備考
STRIPE_SECRET_KEYsk_live_...Stripeダッシュボード [Developers] → [APIキー] で取得
STRIPE_WEBHOOK_SECRETwhsec_... (ライブ用)セクション10.3で作成したエンドポイントの署名シークレット
NEXT_PUBLIC_STRIPE_PRO_PRICE_IDprice_... (ライブ用)セクション10.2.2で作成した本番Price ID
SUPABASE_SERVICE_ROLE_KEYeyJhbG...Webhook処理で使用(変更不要、既存値を確認)
NEXT_PUBLIC_SUPABASE_URLhttps://xxx.supabase.co変更不要(既存値を確認)
NEXT_PUBLIC_SUPABASE_ANON_KEYeyJhbG...変更不要(既存値を確認)
  1. 設定後、再デプロイを実行(環境変数の変更は自動デプロイでは反映されない場合があります)

注意:

  • STRIPE_SECRET_KEYsk_test_... が残っていないか特に注意
  • NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYpk_live_...)はコードで使用されていないため、設定不要です(ホスト型Checkoutアーキテクチャのため)

10.5. 移行後の検証手順

本番環境で実際の決済が正しく動作するか、以下のステップで検証します。

10.5.1. 基本動作確認

  1. https://blueperiod.blue/pricing にアクセスし、Pricingページが表示されることを確認
  2. Pro プランの「Subscribe」ボタンがクリック可能であることを確認
  3. ボタンをクリックし、Stripeの決済ページに遷移することを確認
  4. 実際のクレジットカードで少額決済を実行
  5. /checkout/success に遷移し、検証成功画面が表示されることを確認

10.5.2. Webhook動作確認

  1. Stripeダッシュボード [Developers] → [Webhooks] を開く
  2. ライブ用エンドポイントをクリックし、イベント配信履歴を確認
  3. checkout.session.completedcustomer.subscription.created の両方が 200 で応答されていることを確認
  4. Supabaseダッシュボードで:
    • customers テーブルにレコードが作成されていること
    • subscriptions テーブルにレコードが作成されていること
    • profiles テーブルの is_protrue に更新されていること

10.5.3. カスタマーポータル確認

  1. 設定画面(/settings)にアクセス
  2. 「サブスクリプションを管理」ボタンが表示されることを確認
  3. ボタンをクリックし、Stripeポータルに遷移することを確認
  4. ポータル内で支払い方法の確認・領収書のダウンロードができることを確認

10.5.4. キャンセルフロー確認(オプション)

  1. カスタマーポータルからサブスクリプションをキャンセル
  2. Stripeダッシュボードでキャンセルイベントが配信されたことを確認
  3. Supabaseで is_profalse に戻ることを確認
  4. アプリでPro機能が無効化されることを確認

10.6. 移行チェックリスト

  • Stripeアカウントの本人確認が完了している
  • ライブモードで本番用Priceを作成し、Price IDを控えている
  • ドメイン認証(blueperiod.blue)が完了している
  • カスタマーポータルの設定が完了している
  • ライブモードでWebhookエンドポイント(https://blueperiod.blue/api/stripe/webhook)を作成し、5種類のイベントを登録している
  • Webhook署名シークレットを控えている
  • VercelのProduction環境変数にライブ用の値を設定している
    • STRIPE_SECRET_KEYsk_live_... であることを確認
    • STRIPE_WEBHOOK_SECRET がライブ用の whsec_... であることを確認
    • NEXT_PUBLIC_STRIPE_PRO_PRICE_ID がライブ用の price_... であることを確認
  • 再デプロイを実行した
  • 実際のカードでテスト購読し、Webhookが正常に配信されることを確認した
  • Supabaseの is_pro が正しく更新されることを確認した
  • テストモード用のキー(sk_test_...)がProduction環境に残っていないことを確認した
  • StripeダッシュボードのAPI バージョンが 2025-12-15.clover に設定されていることを確認した

関連リンク:

On this page

15. Stripe サブスクリプション統合ガイド
1. 概要
2. アーキテクチャ
決済フロー
サブスクリプション管理フロー
3. 重要:Stripe API バージョンと破壊的変更
current_period_start/end の場所
4. 無料トライアルの実装
5. Webhook ハンドアリング
【重要】Stripe ダッシュボードの Webhook 設定
【参考】constructEventAsync の使用
同期ロジック (src/lib/stripe/admin-sync.ts)
決済完了後のセッション検証
リアルタイム更新
Webhook による同期の限界
6. 開発とテスト
CLI とダッシュボード Webhook の違い(重要)
ローカル開発 (Stripe CLI)
サンドボックス環境
stripe trigger コマンドについて
ローカル環境で必要な環境変数
7. トラブルシューティング
8. Pro 会員による機能制限 (Feature Gating)
8.1. UI レベルの表示制限
8.2. 設定画面のガード
8.3. バックグラウンド処理のガード
9. トライアル無限利用の防止 (Trial Prevention Logic)
9.1. has_used_trial フラグ
9.2. Checkout 時の拒否ロジック
10. 実稼働(Live モード)への移行
10.1. テストモードとライブモードの違い
10.2. ライブモードの事前準備
10.2.1. Stripeアカウントの本人確認
10.2.2. 本番用Price(価格)の作成
10.2.3. ドメイン認証
10.2.4. カスタマーポータルの設定
10.3. 本番用Webhookエンドポイントの作成
作成手順
Webhookイベントの配信について
テスト用とライブ用のWebhookの共存
10.4. Vercel 環境変数の更新
10.5. 移行後の検証手順
10.5.1. 基本動作確認
10.5.2. Webhook動作確認
10.5.3. カスタマーポータル確認
10.5.4. キャンセルフロー確認(オプション)
10.6. 移行チェックリスト