目次
1. はじめに
現在開発中の飲食店向けSaaSプラットフォームにおいて、Stripeを使用したサブスクリプション機能を実装しました。本記事では、Next.js 15 (App Router) と Prisma を組み合わせ、**「ユーザー登録 → 決済 → アカウント有効化」**という一連のフローをセキュアに構築する方法を解説します。
2. 実装要件とデータベース設計
今回のSaaSモデルでは、以下の4点を必須要件として定義しました。
システム要件
- プランの多段階化: ライト(980円)とスタンダード(2,980円)の提供。
- 即時利用制限: 決済完了まで機能へのアクセスを遮断。
- Webhookによる自動更新: 決済成功・解約時のステータス同期。
- セルフプラン変更: 管理画面からのアップグレード対応。
Prismaによるスキーマ設計
ユーザー(飲食店)モデルにStripe関連のフィールドを統合し、決済状態をDBで一元管理します。
コード スニペット
// schema.prisma
model Restaurant {
id String @id @default(cuid())
// ...基本情報
// セットアップ状況(決済完了でtrue)
isSetupCompleted Boolean @default(false)
// Stripe連携用フィールド
stripeCustomerId String? @unique
subscriptionId String? @unique
subscriptionStatus String? @default("inactive")
subscriptionPlanId String?
}
3. 実装のハイライト
3.1 「仮登録」と「決済」を分離するUX
ユーザー登録と支払い登録を分けることで、離脱を防ぎつつ確実に課金へ誘導します。
- 新規登録後: 自動的に
/admin/subscriptionへリダイレクト。 - Stripe Checkout: サーバーサイドで支払いリンクを生成し、Stripeの堅牢な決済画面へ遷移。
3.2 Webhookによるステータス同期(最重要)
クライアント側の「完了画面」は、通信遮断などで100%の実行が保証されません。そのため、StripeからのWebhookを「唯一の真実」として扱います。
api/webhooks/stripe/route.ts の実装例:
TypeScript
if (event.type === "checkout.session.completed") {
const session = event.data.object as Stripe.Checkout.Session;
await prisma.restaurant.update({
where: { id: restaurantId },
data: {
stripeCustomerId: session.customer as string,
subscriptionId: session.subscription as string,
subscriptionStatus: "active",
isSetupCompleted: true
}
});
}
3.3 未払いユーザーの徹底的なアクセス制御
Next.jsの layout.tsx(サーバーコンポーネント)を活用し、未払い状態では管理メニューそのものをレンダリングさせない仕組みを導入しました。
4. 開発・デバッグ:Stripe CLIの活用
ローカル環境ではWebhookを受け取れないため、Stripe CLIを使用してイベントを転送します。
Bash
# ローカルへのWebhook転送コマンド
stripe listen --forward-to localhost:3000/api/webhooks/stripe
この開発手法により、署名検証(whsec_...)を含めた本番同等のテストが可能になります。
5. まとめ
Next.js 15 と Prisma、そして Stripe を組み合わせることで、以下の成果が得られました。
- 確実な収益化: 決済なしでの利用をシステムレベルで排除。
- 運用の自動化: 入金確認作業のゼロ化。
- スケール性: 容易なプラン追加や変更が可能な柔軟な設計。
SaaS開発において決済は信頼性が命です。この構成は、今後の拡張においても強力な基盤となると確信しています。
コメント