実務・技術解説
マイクロSaaSのStripe決済実装ガイド——「Stripe入れるだけ」では済まない理由
マイクロSaaSにStripe決済を組み込む手順と注意点。Checkout、Webhook、サブスク管理、失敗時のリトライまで、AIコードでは抜け落ちやすい実装ポイントを解説。
「Stripeを入れれば課金できるようになる」——これは正しい。ただし「Stripeを入れるだけ」で本番運用に耐える課金システムになるかというと、まったく別の話だ。
AIツールに「Stripe連携を追加して」と指示すると、基本的なCheckoutへの遷移コードは生成してくれる。でもそこから先——Webhookの処理、カード期限切れの対応、サブスク解約時のアクセス制御、本番環境とテスト環境の切り替え——は、AIが正しく生成してくれることを期待してはいけない。
この記事では、マイクロSaaSにStripe決済を組み込む際に必要な実装ポイントを、AIコードで特に抜け落ちやすい部分を中心に解説する。
決済実装の全体像
マイクロSaaSの決済フローは、以下の要素で構成される。
ユーザー → プラン選択 → Stripe Checkout → 支払い完了
↓
Stripe → Webhook → アプリ
↓
ユーザーのプラン情報を更新
↓
有料機能にアクセス可能に
見た目はシンプルだが、実装すべきポイントは意外と多い。
1. Stripe Checkoutを使う(自前のフォームは作らない)
カード番号を自前のフォームで受け取るのは絶対にやめる。PCI DSSの準拠が必要になり、個人開発者にはハードルが高すぎる。
Stripe Checkoutを使えば、決済フォームはStripe側がホストしてくれる。カード情報はあなたのサーバーを一切経由しない。
AI生成コードで注意すべき点
AIが生成するStripe Checkoutのコードでよくある問題:
- テストキーがハードコードされている — APIキーは環境変数で管理する(クラウド破産を防ぐAPIキー管理術)
- success_urlとcancel_urlが固定 — 環境に応じて動的に変える必要がある
- metadataが空 — Webhookで「どのユーザーの支払いか」を特定するために、user_idをmetadataに入れる
2. Webhookを正しく実装する
ここが決済実装の核心部分であり、AIコードで最も抜け落ちやすい部分だ。
Stripe Checkoutで支払いが完了しても、あなたのアプリは「支払いが完了した」ことを知らない。Stripeからの通知(Webhook)を受け取って、初めてアプリ側でユーザーのプランを更新できる。
最低限処理すべきイベント
| イベント | 意味 | やるべきこと |
|---|---|---|
checkout.session.completed | 支払い完了 | ユーザーのプランを有料に更新 |
invoice.payment_succeeded | 月次課金成功 | サブスク継続を確認 |
invoice.payment_failed | 月次課金失敗 | ユーザーに通知、猶予期間設定 |
customer.subscription.deleted | サブスク解約 | ユーザーのプランを無料に戻す |
Webhook実装の落とし穴
冪等性(べきとうせい)を担保する
Stripeは同じイベントを複数回送信することがある(ネットワークエラー時のリトライ)。同じイベントを2回処理すると、ユーザーに二重課金したりプランが二重に適用されたりする。
対策:Webhook受信時にevent.idを記録し、既に処理済みなら何もしない。
署名検証を行う
Webhookのエンドポイントは公開URLなので、誰でもリクエストを送れてしまう。Stripeの署名ヘッダー(stripe-signature)を検証して、Stripeからの正規のリクエストであることを確認する。
AIが生成するコードでは、この署名検証が省略されていることが非常に多い。省略すると、偽のWebhookを送りつけることでユーザーを不正に有料プランにすることが可能になる。
レスポンスを200で返す
Webhook処理中にエラーが発生しても、StripeにはHTTP 200を返す。500を返すとStripeがリトライを繰り返し、同じイベントが何度も処理される可能性がある。エラーは内部でログに記録して別途対応する。
3. サブスクリプション管理
マイクロSaaSで最も一般的なのはサブスク(月額課金)モデルだ。
→ 各課金モデルの比較は収益化モデル5選
実装が必要な機能
プラン変更(アップグレード/ダウングレード)
ユーザーがプランを変更したとき、日割り計算をどうするか。Stripeのproration設定で制御できるが、デフォルト設定のままだと意図しない挙動になることがある。
解約フロー
即座に解約するか、現在の請求期間の終わりに解約するか。多くのSaaSは「期間終了時に解約」を採用している(cancel_at_period_end)。
カード期限切れ・支払い失敗
カードの有効期限が切れた場合、自動的にサブスクが停止する。ユーザーにカード更新を促すメールを送る仕組みが必要。StripeのSmart Retriesと組み合わせて、自動リトライ+通知メールの設計をする。
4. 有料/無料ユーザーの出し分け
決済を入れたら、アプリ側で「有料ユーザーかどうか」を判定する仕組みが必要になる。
データ設計
Supabaseを使っている場合、最もシンプルな方法はユーザーテーブルにplanカラムを追加すること。
users テーブル
├── id (UUID)
├── email
├── plan ('free' | 'pro' | 'enterprise')
├── stripe_customer_id
└── subscription_status ('active' | 'past_due' | 'canceled')
Webhookでサブスクの状態が変わるたびに、このテーブルを更新する。
アクセス制御
有料機能へのアクセス制御は、フロントエンド(UI表示)とバックエンド(API/RLS)の両方で行う。
フロントエンドだけでの制御は「UIを隠しているだけ」であり、直接APIを叩けば有料機能にアクセスできてしまう。必ずSupabase RLSまたはRBACでバックエンド側でも制御する。
5. テスト環境と本番環境の切り分け
Stripeにはテストモードと本番モードがある。開発中はテストモードのAPIキーを使い、本番ではライブモードのAPIキーに切り替える。
よくある事故
- テストキーのまま本番にデプロイ(課金できない)
- 本番キーで開発環境をテスト(実際に課金が発生する)
- Webhookのエンドポイントがテスト環境を指したまま(本番の決済通知がテスト環境に行く)
環境変数でSTRIPE_SECRET_KEYとSTRIPE_WEBHOOK_SECRETを環境ごとに切り替える。Vercelならプレビュー環境とプロダクション環境で別々の値を設定できる。
6. 法的対応
日本でサブスクリプションサービスを提供する場合、特定商取引法に基づく表記が必要。最低限以下を記載する。
- 事業者名
- 連絡先
- 料金と支払い方法
- 解約方法
- 返金ポリシー
これはコードの問題ではないが、ページとして用意しないと法的リスクがある。
まとめ:決済は「入れるだけ」では動かない
| 実装ポイント | AIが生成するか | 自分で実装が必要か |
|---|---|---|
| Checkout遷移 | ○ | △(キー管理、metadata) |
| Webhook受信 | △(基本形のみ) | ◎(署名検証、冪等性) |
| サブスク管理 | × | ◎ |
| プラン出し分け | × | ◎ |
| 環境切り替え | × | ◎ |
| 法的対応 | × | ◎ |
決済周りのバグは直接的に金銭トラブルに繋がる。プロに任せるべきタイミングの見極めが特に重要な領域だ。