IT

【Stripe】Next.js 15とPrismaで実装するサブスクリプション機能の開発

Next.js 15(App Router)とPrismaで、Stripeのサブスクリプションを実装した記録です。登録と決済の分け方、Webhookでの状態の同期、未払い時のアクセス制御、Stripe CLIでのローカル確認を紹介します。

この記事の結論:Next.js 15(App Router)とPrismaでStripeのサブスクリプションを作るときは、ユーザー登録と決済を分けてStripe Checkoutの支払い画面に送り、決済状態の更新はStripeのWebhook(checkout.session.completed など)を正として扱います。決済状態はPrismaのモデルにまとめて持ち、未払いのあいだは layout.tsx(サーバーコンポーネント)で管理画面を表示しないようにします。ローカルでは、Stripe CLIの stripe listen --forward-to でWebhookを受け取って確かめます。

1. はじめに

現在開発中の飲食店向けSaaSプラットフォームにおいて、Stripeを使用したサブスクリプション機能を実装しました。本記事では、Next.js 15 (App Router) と Prisma を組み合わせ、「ユーザー登録 → 決済 → アカウント有効化」という一連のフローをセキュアに構築する方法を解説します。


2. 実装要件とデータベース設計

今回のSaaSモデルでは、以下の4点を必須要件として定義しました。

システム要件

  1. プランの多段階化: ライト(980円)とスタンダード(2,980円)の提供。
  2. 即時利用制限: 決済完了まで機能へのアクセスを遮断。
  3. Webhookによる自動更新: 決済成功・解約時のステータス同期。
  4. セルフプラン変更: 管理画面からのアップグレード対応。

Prismaによるスキーマ設計

ユーザー(飲食店)モデルにStripe関連のフィールドを統合し、決済状態をDBで一元管理します。

schema.prismaprisma
// 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 の実装例:

ts
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_...)を含めた本番同等のテストが可能になります。


株式会社bundlyzeでは、予約管理システムを企画から運用まで開発し、LINE公式アカウントと連携する仕組みも自社で開発しています。決済のように外部のサービスとつなぐ部分を含めた開発の進め方は、業務システム開発のページで紹介しています。

5. まとめ

Next.js 15 と Prisma、そして Stripe を組み合わせることで、以下の成果が得られました。

  • 確実な収益化: 決済なしでの利用をシステムレベルで排除。
  • 運用の自動化: 入金確認作業のゼロ化。
  • スケール性: 容易なプラン追加や変更が可能な柔軟な設計。

SaaS開発において決済は信頼性が命です。この構成は、今後の拡張においても強力な基盤となると確信しています。

よくある質問

決済が完了したかどうかは、どこで判定すればいい?

StripeからのWebhookで判定します。クライアント側の完了画面は、通信が切れるなどで必ず実行されるとは限らないため、checkout.session.completed を受け取ったときにデータベースの決済状態を更新します。

ローカル環境でStripeのWebhookを試すには?

Stripe CLIの stripe listen --forward-to localhost:3000/api/webhooks/stripe で、イベントをローカルに転送します。署名検証(whsec_...)を含めて、本番と同じ流れで確かめられます。

未払いのユーザーに管理画面を見せないようにするには?

Next.jsの layout.tsx(サーバーコンポーネント)で決済状態を確かめ、未払いのあいだは管理メニューそのものを表示しないようにします。