Stripe サブスクリプション統合ガイド
Stripeを用いたProプラン登録と決済機能の統合手順
15. Stripe サブスクリプション統合ガイド
1. 概要
本プロジェクトでは、Stripe を使用して「BluePeriod Pro」サブスクリプションプランを提供しています。ユーザーは Pro プランに加入することで、クラウド同期、無制限の AI 執筆アシスタント、キャラクターチャットなどの高度な機能を利用できます。
2. アーキテクチャ
決済フローは Stripe ホスト型の Checkout API をベースにしており、Supabase DB との同期は Webhook によって行われます。
決済フロー
- Checkout セッション作成: アプリ内から
/api/stripe/checkout(POST) を呼び出し、Stripe の決済ページへリダイレクトします。 - 決済完了: Stripe のページでユーザーが操作を完了すると、
/checkout/successへ戻ります。 - 非同期同期 (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 ランタイム環境では constructEvent と constructEventAsync の両方が利用可能ですが、本プロジェクトでは一貫して 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_change→handle_pro_status_update():statusが'active'または'trialing'の場合にis_pro = true、それ以外はis_pro = false - トリガー
on_subscription_trial_check→handle_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日) |
| 用途 | ローカル開発中のみ | 本番環境・デプロイ済みテスト |
| Secret | CLI 起動時に発行される一時的な 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- 出力された
webhook signing secret (whsec_...)を.env.localのSTRIPE_WEBHOOK_SECRETに設定します。 - Stripe 管理画面で設定されている API バージョンと、ローカルのライブラリ(
stripenpm 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.createにpayment_method_typesを指定せず、Stripe ダッシュボード側で「自動決済方法」を管理するのが現在の推奨です。また、Link はlocalhostでは表示されず、デプロイ済みのドメインが必要です。 - "Supabase admin environment variables are not set": ローカル環境で Webhook を受信したが、
SUPABASE_SERVICE_ROLE_KEYが.env.localに設定されていない。同環境変数を追加してください。 - Webhook 配信が失敗する(Stripe ダッシュボードで確認): Vercel の環境変数(特に
STRIPE_WEBHOOK_SECRETとSUPABASE_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_proがtrueかつ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 ID | price_1xxx...(テスト用) | price_1yyy...(ライブ用、別ID) |
| Webhook署名シークレット | whsec_...(テスト用) | whsec_...(ライブ用、別の値) |
重要: テストモードで作成したPrice IDやWebhook署名シークレットは、ライブモードでは使用できません。各モードで独立して作成・設定する必要があります。
10.2. ライブモードの事前準備
10.2.1. Stripeアカウントの本人確認
- ダッシュボード右上に「本人確認が必要」などの警告が表示されていないか確認
- 未完了の場合は、Stripeからの入金(ペイアウト)が保留されます
- 必要書類: 身分証明書、銀行口座情報、事業情報など
10.2.2. 本番用Price(価格)の作成
ライブモードに切り替えてから、Pro プランの価格を作成します。
- ダッシュボード右上のトグルを 「ライブモード」 に切り替え
- [商品カタログ] → [商品を追加] をクリック
- 以下を入力:
- 商品名:
BluePeriod Pro - 説明:
BluePeriod Pro サブスクリプション - 価格: 月額料金を設定(例: ¥980/月 または $9.99/月)
- 課金サイクル:
定額→月次
- 商品名:
- 作成完了後、表示される Price ID (
price_...) を控える
10.2.3. ドメイン認証
- [Settings] → [Payments] → [Apple Pay / Google Pay] を開く
blueperiod.blueを登録- ドメインの所有権確認が求められた場合は、DNSレコードまたはファイル配置で認証
10.2.4. カスタマーポータルの設定
- [Settings] → [Billing] → [Customer portal] を開く
- 以下の機能の有効/無効を設定:
- サブスクリプションのキャンセル: 許可する場合、「Allow customers to cancel subscriptions」を有効化
- 支払い方法の変更: 原則として許可を推奨
- 請求書のダウンロード: 許可を推奨
- [Save] をクリック
10.3. 本番用Webhookエンドポイントの作成
Webhookとは、Stripe側でイベント(決済完了、サブスクリプション変更など)が発生した際に、StripeサーバーからアプリサーバーへHTTP POSTリクエストを送信する仕組みです。
テスト環境ではStripe CLIがローカルにイベントを転送していましたが、本番ではStripeダッシュボードでエンドポイントURLを登録し、Stripeが直接アプリサーバーにリクエストを送ります。
作成手順
- ダッシュボード右上のトグルを 「ライブモード」 に切り替え
- [Developers] → [Webhooks] を開く
- [エンドポイントを追加] をクリック
- 以下を入力:
- エンドポイントURL:
https://blueperiod.blue/api/stripe/webhook - リッスンするイベント: 以下の 5種類をすべて追加
checkout.session.completed— 決済完了時customer.subscription.created— サブスクリプション作成時customer.subscription.updated— サブスクリプション更新時customer.subscription.deleted— サブスクリプション削除(キャンセル完了)時invoice.payment_succeeded— 支払い成功時(継続課金の確認)
- エンドポイントURL:
- [エンドポイントを追加] をクリック
- 作成完了後、エンドポイントの詳細画面を開く
- 「署名シークレット」 の [表示] をクリックし、
whsec_...の値を控える
この
whsec_...がSTRIPE_WEBHOOK_SECRET環境変数の値になります。 エンドポイントごとに一意であり、テスト用のものとは異なります。
Webhookイベントの配信について
- Stripeはイベント発生後、最大3日間リトライします(指数バックオフ)
- ダッシュボードの [Developers] → [Webhooks] → [エンドポイント] から配信履歴と成功/失敗を確認できます
- 失敗したイベントは [再送] ボタンで手動再送が可能です
テスト用とライブ用のWebhookの共存
テスト用Webhookエンドポイント(https://blueperiod.blue/... をテストモードで登録したもの)とライブ用は独立しています。両方を登録したままでも問題ありません。テストモードのイベントはテスト用エンドポイントに、ライブモードのイベントはライブ用エンドポイントに配信されます。
10.4. Vercel 環境変数の更新
本番環境のVercelにライブモード用の環境変数を設定します。
- Vercelダッシュボード → プロジェクト → [Settings] → [Environment Variables] を開く
- Production 環境に対して以下を設定:
| 環境変数 | 設定する値 | 備考 |
|---|---|---|
STRIPE_SECRET_KEY | sk_live_... | Stripeダッシュボード [Developers] → [APIキー] で取得 |
STRIPE_WEBHOOK_SECRET | whsec_... (ライブ用) | セクション10.3で作成したエンドポイントの署名シークレット |
NEXT_PUBLIC_STRIPE_PRO_PRICE_ID | price_... (ライブ用) | セクション10.2.2で作成した本番Price ID |
SUPABASE_SERVICE_ROLE_KEY | eyJhbG... | Webhook処理で使用(変更不要、既存値を確認) |
NEXT_PUBLIC_SUPABASE_URL | https://xxx.supabase.co | 変更不要(既存値を確認) |
NEXT_PUBLIC_SUPABASE_ANON_KEY | eyJhbG... | 変更不要(既存値を確認) |
- 設定後、再デプロイを実行(環境変数の変更は自動デプロイでは反映されない場合があります)
注意:
STRIPE_SECRET_KEYにsk_test_...が残っていないか特に注意NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY(pk_live_...)はコードで使用されていないため、設定不要です(ホスト型Checkoutアーキテクチャのため)
10.5. 移行後の検証手順
本番環境で実際の決済が正しく動作するか、以下のステップで検証します。
10.5.1. 基本動作確認
https://blueperiod.blue/pricingにアクセスし、Pricingページが表示されることを確認- Pro プランの「Subscribe」ボタンがクリック可能であることを確認
- ボタンをクリックし、Stripeの決済ページに遷移することを確認
- 実際のクレジットカードで少額決済を実行
/checkout/successに遷移し、検証成功画面が表示されることを確認
10.5.2. Webhook動作確認
- Stripeダッシュボード [Developers] → [Webhooks] を開く
- ライブ用エンドポイントをクリックし、イベント配信履歴を確認
checkout.session.completedとcustomer.subscription.createdの両方が 200 で応答されていることを確認- Supabaseダッシュボードで:
customersテーブルにレコードが作成されていることsubscriptionsテーブルにレコードが作成されていることprofilesテーブルのis_proがtrueに更新されていること
10.5.3. カスタマーポータル確認
- 設定画面(
/settings)にアクセス - 「サブスクリプションを管理」ボタンが表示されることを確認
- ボタンをクリックし、Stripeポータルに遷移することを確認
- ポータル内で支払い方法の確認・領収書のダウンロードができることを確認
10.5.4. キャンセルフロー確認(オプション)
- カスタマーポータルからサブスクリプションをキャンセル
- Stripeダッシュボードでキャンセルイベントが配信されたことを確認
- Supabaseで
is_proがfalseに戻ることを確認 - アプリでPro機能が無効化されることを確認
10.6. 移行チェックリスト
- Stripeアカウントの本人確認が完了している
- ライブモードで本番用Priceを作成し、Price IDを控えている
- ドメイン認証(
blueperiod.blue)が完了している - カスタマーポータルの設定が完了している
- ライブモードでWebhookエンドポイント(
https://blueperiod.blue/api/stripe/webhook)を作成し、5種類のイベントを登録している - Webhook署名シークレットを控えている
- VercelのProduction環境変数にライブ用の値を設定している
-
STRIPE_SECRET_KEYがsk_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に設定されていることを確認した
関連リンク:
- 15_stripe_architecture — Stripe実装アーキテクチャ(System文書)
- 04_database_schema_supabase — データベーススキーマ
- 06_development_guidelines — 開発ガイドライン
- 09_design_system — デザインシステム
- 2026-02-16_1400_issue_stripe-phase6-production-ready-and-bugfixes
- 2026-02-16_1400_plan_stripe-phase6-production-ready-and-bugfixes
- 2026-04-06_stripe-implementation-audit — Stripe実装 全面監査レポート
- 2026-04-06_1900_report_stripe-audit-remediation — 監査修正の完了報告