メインコンテンツにスキップ
株式会社ゼットリンカー - Next.js システム開発専門
AI開発

Next.js×Stripeでサブスク課金を内製する設計パターン|Webhookの罠と失敗しない実装

Conclusion

サブスク課金は「Checkout Sessionsで入口→Webhookで状態確定→DB反映」の3層構成が基本。更新・解約・支払い失敗も同じ優先度で設計します。

サブスク課金はCheckout Sessionsで入口を作り、Webhookを決済成立の正として状態を同期し、Firestoreにローカル状態を持つ3層構成で内製できます。更新・解約・支払い失敗の設計と費用感、失敗パターンまで情シス担当向けに整理しました。

17分で読めます
Next.jsNext.jsStripe中小企業FirebaseFirebaseサブスク課金

サブスクリプション課金は、「毎月同じ処理を自動で繰り返す」という一点だけで、単発の決済とは設計の難易度が変わります。本記事では、Next.js 16 + Stripe でサブスク課金を内製する場合の実務設計を、2026年7月時点の情報で整理します。

先に結論です。

  • サブスク課金の内製は「Checkout Sessionsで決済入口を作る」「Webhookを正として状態を同期する」「ローカルDBにサブスク状態を持つ」の3点を押さえれば、Next.js 16 + Firebase構成でも数週間規模で着手できます
  • 決済の成否はリダイレクト結果ではなくWebhookイベントで判定するのが2026年時点の標準的な設計で、ここを誤ると「決済したのに使えない」「解約したのに課金が続く」という事故につながります
  • 更新・解約・支払い失敗という3つの継続イベントをそれぞれ設計しておかないと、初回課金だけ動いて運用が破綻します

こんな状況ではありませんか?

  • 自社サービスに月額課金を導入したいが、Stripeの導入記事は情報が断片的で、どこまで自前で作ればいいのか判断がつかない
  • 決済代行SaaSやプラットフォーム型の課金機能では、自社独自の料金プランやトライアル条件を再現できない
  • 「決済は成功したのに管理画面に反映されない」「解約したのに翌月も請求が来た」といった事故を、自分たちのサービスでは絶対に起こしたくない

この記事で分かること

  • Next.js 16 + Stripeでサブスク課金を内製するときの3層構成(Checkout・Webhook・ローカル状態)
  • 更新・解約・支払い失敗という継続イベントの扱い方と、見落としやすい落とし穴
  • 費用感・導入手順の目安と、実装でつまずきやすい失敗パターン

自社サービスに課金機能を持たせたいが、既製の決済代行だけでは自社の料金設計に合わないと感じている情シス担当・開発責任者の方へ——Next.js 16でサブスク課金を内製する場合の設計を、順に整理します。ただし1点、Webhookの実装だけ気をつけないと初回課金しか動かない落とし穴があるので、これは後半で詳しく説明します。

なぜサブスク課金は単発決済より設計が難しいのか?

単発決済は「1回の成功・失敗」で完結しますが、サブスクは「毎月自動で繰り返される状態」を管理し続ける必要があるためです。

物販の単発決済であれば、決済が成功した瞬間に商品を発送すれば処理は完結します。ところがサブスクリプションは、初回課金が終わった後も「来月も課金されるか」「途中で解約されたか」「カードの有効期限切れで支払いが失敗していないか」という状態が、ユーザーの操作なしに動き続けます。この“動き続ける状態”をアプリ側でどう捕まえるかが、サブスク課金の実装で最も設計判断が集中する部分です。

Stripeでは、この継続的な状態変化を「Webhookイベント」として通知する仕組みが用意されています。2026年7月時点のStripe Billingは、AI利用量など従量課金の計測機能も強化されていますが(出典: Stripe公式ドキュメント Billing概要)、月額固定のシンプルなサブスクであれば、Checkout SessionsとWebhookの組み合わせが基本形になる点は変わりません。

一方で、決済代行SaaSやノーコード課金ツールで足りるケースも当然あります。料金プランが1〜2種類で、トライアル条件やプラン変更のロジックが単純なら、既製ツールのほうが早く安全です。内製が効くのは、「自社独自の料金体系がある」「サブスク状態と自社のユーザー権限を密に連携させたい」場合です。この判断軸は、受発注管理をSaaSからフルスクラッチに切り替える判断基準で整理した考え方とも重なります。

Next.jsでどう設計するか?3層構成の全体像

「Checkout Sessions(決済入口)」「Webhook(状態の正)」「ローカルDB(サブスク状態のコピー)」の3層で、最小構成は完結します。

サブスク課金の3層構成図。ユーザーの決済操作がCheckout Sessionsを起点にStripe側で処理され、Webhookイベントがアプリのサーバーに通知され、Firestoreのサブスク状態が更新され、その状態に応じてアプリの機能アクセスが制御される流れを示す

Checkout Sessions:決済の入口を作る

ユーザーが「登録する」ボタンを押した瞬間の処理です。Route HandlerでCheckout Sessionを作成し、Stripeがホストする決済ページへリダイレクトします。

// app/api/checkout/route.ts
import Stripe from "stripe";
import { auth } from "@/lib/firebase-admin";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: Request) {
  const { priceId, uid } = await req.json();

  const session = await stripe.checkout.sessions.create({
    mode: "subscription",
    line_items: [{ price: priceId, quantity: 1 }],
    // ユーザーIDをmetadataに埋め込み、Webhook側でFirestoreのどのユーザーか特定する
    subscription_data: { metadata: { firebaseUid: uid } },
    success_url: `${process.env.APP_URL}/dashboard?checkout=success`,
    cancel_url: `${process.env.APP_URL}/pricing`,
  });

  return Response.json({ url: session.url });
}

ポイントは、subscription_data.metadata にFirebaseのユーザーIDを埋め込んでおくことです。これがないと、Webhook側で「どのStripe顧客が自社のどのユーザーに対応するか」を突き合わせられません。

Webhook:決済の成否はここで判定する

リダイレクトの成功画面ではなく、Webhookイベントを「決済が成立した」の正とするのが2026年時点の標準的な設計です。

success_url にリダイレクトされたからといって、決済が確実に完了しているとは限りません。ブラウザが閉じられる、ネットワークが切れるといった事情でリダイレクトが失敗しても、Stripe側では決済が成立していることがあります。Webhookは、そうした経路に依存しない「サーバー間の通知」なので、状態確定の唯一の正として扱います。

// app/api/webhooks/stripe/route.ts
import Stripe from "stripe";
import { db } from "@/lib/firebase-admin";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;

export async function POST(req: Request) {
  const body = await req.text();
  const signature = req.headers.get("stripe-signature")!;

  // 署名検証を必ず行う。生のリクエストボディに対して検証する点が重要
  const event = stripe.webhooks.constructEvent(body, signature, webhookSecret);

  switch (event.type) {
    case "checkout.session.completed": {
      // 初回課金の成立。ここでユーザーのプランを有効化する
      break;
    }
    case "invoice.paid": {
      // 毎月の更新課金もこのイベントで届く。ここを見落とすと2ヶ月目以降が動かない
      break;
    }
    case "customer.subscription.deleted": {
      // 解約確定。cancel_at_period_end の期間満了後に届く
      break;
    }
    case "invoice.payment_failed": {
      // 支払い失敗。即座に機能を止めるかは猶予設計次第
      break;
    }
  }

  return Response.json({ received: true });
}

このコードで一番大事な行は stripe.webhooks.constructEvent による署名検証です。生のリクエストボディに対して検証する必要があるため、Route Handlerで req.text() を使い、JSONパース前の文字列のまま渡す点に注意します。ここをJSONにパースしてから検証しようとすると署名が一致せず、Webhookそのものが機能しません。

ローカルDB:Stripeに毎回問い合わせない

サブスクの状態(有効/解約予定/停止中)は、リクエストのたびにStripe APIへ問い合わせるのではなく、Webhookで受け取った内容をFirestoreに保存しておき、アプリ側はそのコピーを参照します。Stripeへの都度問い合わせは、レスポンス速度の悪化とAPIレート制限の両方のリスクがあるため、「Webhookで受けた状態をローカルに反映し、アプリはローカルを見る」という一方向の同期にするのが基本です。

サブスク状態にひもづく機能アクセス制御は、Firebase Authenticationのカスタムクレームやfirestoreのユーザードキュメントで管理するのが扱いやすい方法です。認証まわりの基本設計は、Next.js 16 + Firebase Authenticationの会員制サイト構築事例の構成がそのまま流用できます。

ここで一度お知らせ

15分のカジュアル相談

事例・要件未定でもOK/営業はしません/所要時間15分

カジュアルに相談する

更新・解約・支払い失敗をどう設計するか?

初回課金だけでなく、更新・解約・支払い失敗という3つの継続イベントをそれぞれ個別に設計する必要があります。

サブスク課金でつまずきやすいのは、初回の checkout.session.completed だけを実装して満足してしまうケースです。実際の運用で必要になるイベントは、大きく3種類あります。

イベント内容設計のポイント
invoice.paid毎月の更新課金が成立このイベントを受けて初めて「今月分も有効」と確定する。初回だけでなく毎月届く前提で実装する
customer.subscription.deleted解約が確定Stripeは即時解約ではなく、cancel_at_period_end: true にした上で期間満了時にこのイベントを送る設計になっている。解約ボタンを押した瞬間に機能を止めると、支払い済みの期間分をユーザーから奪うことになる
invoice.payment_failedカード期限切れ等で支払いが失敗即座に停止するか、数日の猶予期間を設けるかは自社のポリシー次第。猶予期間中は「支払い方法の更新をお願いする」通知を出す設計が一般的

とくに解約まわりは、ユーザー体験に直結する設計判断です。「解約ボタンを押した瞬間に使えなくなる」設計は、すでに支払い済みの期間があるユーザーにとって不利益になります。Stripeのデフォルト挙動である「期間満了まで使える」に合わせてアプリ側の権限判定を実装するほうが、問い合わせやクレームの発生を抑えられます。

いくらで作れるか?費用感の目安

最小構成なら初期開発は数週間規模の開発サイクル、Stripe側の手数料以外の運用コストは無料枠〜数千円程度が目安です。

項目内容目安
初期開発Checkout・Webhook・ローカル状態同期・権限制御数週間の開発サイクル
Stripe手数料決済ごとの決済手数料(プラン内で完結)Stripeの公表料率に準拠
ホスティングVercel + Firebase小規模なら無料枠〜月数千円
保守料金プラン変更・Webhookイベント追加時の改修都度数時間〜数日程度

決済代行SaaSやサブスク管理プラットフォームは、月額固定費に加えて決済額の数%を上乗せする料金体系が多く、決済件数が少なければそちらのほうが総コストで有利です。内製の費用対効果が出るのは、自社独自の料金プラン設計やユーザー権限との密な連携など、「プラットフォームの型に自社サービスを合わせるコスト」のほうが大きい場合です。

導入手順と期間の目安

料金プランの整理からWebhookの本番切り替えまで5ステップ、合計1〜2ヶ月程度が現実的な目安です。

  1. 料金プランの整理(1週間程度):月額プランの種類、トライアル条件、プラン変更のルールを洗い出す
  2. Stripe側のプラン設定とCheckout実装(1〜2週間):Stripe Dashboardで価格を作成し、Checkout Sessionsの作成処理を実装する
  3. Webhook実装とローカル状態同期(1〜2週間):署名検証・主要4イベントの処理・Firestoreへの反映を実装する
  4. 権限制御とマイページ実装(1週間程度):サブスク状態に応じた機能アクセス制御、解約・プラン変更のUIを実装する
  5. 本番Webhookの切り替えとテスト(1週間程度):Stripe CLIでのローカル検証を経て、本番エンドポイントへ切り替える

最初から複数プランやアドオン課金まで手を広げず、まず単一プランの月額課金でWebhookの一連の流れを安定させてから拡張するのが、手戻りの少ない順序です。

失敗しやすいパターンと回避策

  • invoice.paid を実装せず初回課金だけで満足する:2ヶ月目以降の更新課金が反映されず、支払っているのにサービスが止まる事故につながります。継続課金のイベントも初回課金と同じ優先度で実装する
  • Webhookの署名検証を省略する:外部から偽装リクエストを送られても受理してしまいます。stripe.webhooks.constructEvent による検証を必ず行う
  • リダイレクト成功画面だけを見て決済成立と判断する:ブラウザが閉じられた場合などにアプリ側の状態が更新されません。Webhookを正として状態を確定させる
  • 解約ボタンで即座に機能を止める:支払い済み期間の権利をユーザーから奪う設計になります。Stripeの cancel_at_period_end の挙動に合わせる
  • ローカルの状態を持たずAPIに毎回問い合わせる:レスポンス速度の悪化とレート制限のリスクを抱えます。Webhookで受けた状態をローカルDBに保存し、アプリはローカルを参照する

AI駆動開発で何が変わるか?

Webhookのイベント分岐やFirestoreへの反映処理は仕様化しやすく、Claude CodeなどのAIコーディング支援で実装を進めやすい領域です。

Stripeの各イベントに対して「どのFirestoreフィールドをどう更新するか」を先に表として整理してしまえば、その表をそのままAIへの実装指示に落とし込めます。私たちの開発でも、Webhookのイベントハンドラやテストコードの実装はClaude Codeに任せ、料金プランの設計やキャンセルポリシーといった業務判断に人の時間を使う分担で進めています。どこまでをAIに任せ、どこからを人が判断するかという線引きは、見積書・請求書の自動作成をNext.js×AIで内製する実務設計で扱った「計算をAIの生成に混ぜない」という考え方と同じ発想です。

ゼットリンカーでの進め方

私たちが受託で関わった小規模プロジェクトの範囲では、まず現行の料金体系とトライアル条件を整理していただき、単一プランでの最小構成から着手しています。Webhookの主要4イベント(初回課金・更新・解約・支払い失敗)を先に安定させたうえで、プラン追加やアドオン課金を後から拡張していく進め方です。開発の流れと料金の考え方はサービス紹介の料金セクションにまとめています。

よくある質問

Q. 決済代行SaaSではなく内製を選ぶ基準はありますか?

料金プランが1〜2種類でトライアル条件もシンプルなら、既製の決済代行やサブスク管理プラットフォームのほうが早く安全です。自社独自の料金プラン設計や、サブスク状態と自社のユーザー権限を密に連携させたい場合は、内製の費用対効果が出やすくなります。

Q. Webhookの実装で一番気をつけることは何ですか?

署名検証を必ず行うことと、初回課金の checkout.session.completed だけでなく invoice.paid(更新)・customer.subscription.deleted(解約)・invoice.payment_failed(支払い失敗)まで実装することです。更新イベントを見落とすと、2ヶ月目以降の課金がアプリ側に反映されなくなります。

Q. 解約はどのタイミングで機能を止めるべきですか?

Stripeのデフォルト挙動は、解約操作の時点では cancel_at_period_end: true を設定し、実際の権限剥奪は現在の請求期間が満了した後です。解約ボタンを押した瞬間に機能を止める設計は、支払い済み期間の権利をユーザーから奪うことになるため、Stripeの挙動に合わせて期間満了まで機能を維持する設計を推奨します。

Q. サブスク状態はStripe APIに都度問い合わせるべきですか?

推奨しません。リクエストのたびにStripe APIへ問い合わせると、レスポンス速度の悪化とAPIレート制限のリスクが生じます。Webhookで受け取った状態をFirestoreなどのローカルDBに保存し、アプリ側はそのコピーを参照する設計が基本です。

Q. どれくらいの期間と費用で作れますか?

単一プランの最小構成であれば、料金プランの整理から本番Webhookの切り替えまで合計1〜2ヶ月程度が目安です。運用コストはホスティングが無料枠〜月数千円程度、Stripeの決済手数料は公表料率に準じます。

まとめ

  • サブスク課金は「Checkout Sessionsで入口を作り、Webhookを状態の正とし、ローカルDBに状態を持つ」の3層構成から始めるのが現実的です。
  • 更新・解約・支払い失敗という継続イベントを、初回課金と同じ優先度で設計してください。
  • まず単一プランの月額課金でWebhookの流れを安定させ、そこからプラン追加やアドオン課金へ拡張していきましょう。

Webhookのイベント設計や料金プランの整理から一緒に進めたい方には、15分のカジュアル相談(事例・要件未定でもOK/営業はしません)をご用意しています。料金プランの輪郭が見えている方は、要件整理から始める無料相談(1時間・準備不要)からご相談ください。

※本記事に記載したAPIの仕様は、2026年7月時点の公開情報に基づく整理です。実装にあたっては、必ずStripe公式ドキュメントで最新の内容をご確認ください。

本記事は Next.js 16.x 時点の情報です

最終更新:2026年7月21日

Share this article

要件整理から始める無料相談

1時間/準備不要/見積もりまでご案内します

無料相談を申し込む

次に読む記事

ALL ARTICLES →

プライバシーポリシー

株式会社ゼットリンカー(以下、「当社」といいます。)は、お客様の個人情報の重要性を認識し、 その保護の徹底を図るため、以下のプライバシーポリシー(以下、「本ポリシー」といいます。)を定めます。

1. 個人情報の定義

本ポリシーにおいて「個人情報」とは、生存する個人に関する情報であって、 当該情報に含まれる氏名、生年月日その他の記述等により特定の個人を識別することができるもの、 及び他の情報と容易に照合することができ、それにより特定の個人を識別することができることとなるものを指します。

2. 個人情報の収集

当社は、お客様が当社のサービスをご利用になる際、お客様の個人情報を収集することがあります。収集する個人情報は以下の通りです:

  • 氏名
  • メールアドレス
  • 電話番号
  • 会社名・組織名
  • 住所
  • その他当社が定める入力フォームにお客様が入力する情報

3. 個人情報の利用目的

当社は、お客様からご提供いただいた個人情報を、以下の目的で利用します:

  • お客様への連絡やサービスの提供
  • お客様からのお問い合わせへの対応
  • 当社サービスの改善や新サービスの開発
  • メールマガジンの配信(お客様の同意がある場合)
  • 契約や法令等に基づく権利の行使や義務の履行
  • その他、上記利用目的に付随する目的

4. 個人情報の第三者提供

当社は、以下の場合を除き、お客様の同意なく個人情報を第三者に提供することはありません:

  • 法令に基づく場合
  • 人の生命、身体または財産の保護のために必要がある場合であって、本人の同意を得ることが困難である場合
  • 公衆衛生の向上または児童の健全な育成の推進のために特に必要がある場合
  • 国の機関もしくは地方公共団体またはその委託を受けた者が法令の定める事務を遂行することに対して協力する必要がある場合

5. 個人情報の管理

当社は、お客様の個人情報を正確かつ最新の状態に保ち、個人情報への不正アクセス、 個人情報の紛失、破損、改ざん及び漏洩などを防止するため、 セキュリティシステムの維持・管理体制の整備等の必要な措置を講じ、安全対策を実施し個人情報の厳重な管理を行います。

6. 個人情報の開示・訂正・削除

お客様は、当社に対してご自身の個人情報の開示を求めることができます。 また、開示の結果、個人情報の内容が事実でないことが判明した場合には、 速やかに訂正または削除に応じます。

7. 導入事例の公開について

当社は、お客様から依頼いただいたプロジェクトを導入事例として記事にさせていただく場合があります。導入事例として公開する場合は、以下の点に配慮いたします:

  • 機密情報や個人情報は公開いたしません
  • 社名を公開する場合は、公開内容について事前にお客様に確認いただきます
  • 社名を非公開とする場合でも、お客様のご要望に応じて公開内容を確認いただくことが可能です

8. Cookie(クッキー)の使用について

当社のウェブサイトでは、お客様により良いサービスを提供するため、Cookie を使用することがあります。 Cookie により個人を識別できる情報を収集することはありません。 お客様はブラウザの設定により Cookie の受信を拒否することができます。

9. SSL(Secure Socket Layer)について

当社のウェブサイトはSSLに対応しており、ウェブブラウザとウェブサーバーとの通信を暗号化しています。 お客様が入力する個人情報は自動的に暗号化されて送受信されるため、 万が一、第三者が傍受した場合でも内容を解読することは困難です。

10. プライバシーポリシーの変更

当社は、必要に応じて、本ポリシーの内容を変更することがあります。 変更後のプライバシーポリシーについては、当社ウェブサイトに掲載したときから効力を生じるものとします。

11. お問い合わせ

本ポリシーに関するお問い合わせは、以下の窓口までお願いいたします。

株式会社ゼットリンカー

〒160-0023

東京都新宿区西新宿3丁目3番13号西新宿水間ビル2F

代表取締役: 金原隆利

お問い合わせ先: info@zetlinker.com

制定日:2024年1月1日

最終改訂日:2026/7/21

利用規約

この利用規約(以下、「本規約」といいます。)は、株式会社ゼットリンカー(以下、「当社」といいます。)が 提供するウェブサイトおよびサービス(以下、「本サービス」といいます。)の利用条件を定めるものです。 お客様は、本規約に同意した上で、本サービスをご利用ください。

第1条(適用)

1. 本規約は、お客様と当社との間の本サービスの利用に関わる一切の関係に適用されるものとします。

2. 当社は本サービスに関し、本規約のほか、ご利用にあたってのルール等、各種の定め(以下、「個別規定」といいます。)を することがあります。これら個別規定はその名称のいかんに関わらず、本規約の一部を構成するものとします。

3. 本規約の規定と個別規定の規定が異なる場合は、個別規定において特段の定めなき限り、個別規定の規定が優先されるものとします。

第2条(定義)

本規約において使用する以下の用語は、各々以下に定める意味を有するものとします。

  • 「利用契約」とは、本規約を契約条件として当社とお客様との間で締結される、本サービスの利用契約
  • 「知的財産権」とは、著作権、特許権、実用新案権、意匠権、商標権その他の知的財産権(それらの権利を取得し、またはそれらの権利につき登録等を出願する権利を含みます。)
  • 「投稿データ」とは、お客様が本サービスを利用して投稿その他送信するコンテンツ(文章、画像、動画その他のデータを含みますがこれらに限りません。)

第3条(本サービスの提供)

1. お客様は、本規約に同意の上、当社の定める方法によって利用登録を申請し、当社がこれを承認することによって、本サービスを利用することができるようになります。

2. 当社は、お客様に以下のいずれかの事由があると判断した場合、利用登録の申請を承認しないことがあり、その理由については一切の開示義務を負わないものとします。

  • 利用登録の申請に際して虚偽の事項を届け出た場合
  • 本規約に違反したことがある者からの申請である場合
  • その他、当社が利用登録を相当でないと判断した場合

第4条(ユーザーIDおよびパスワードの管理)

1. お客様は、自己の責任において、本サービスのユーザーIDおよびパスワードを適切に管理するものとします。

2. お客様は、いかなる場合にも、ユーザーIDおよびパスワードを第三者に譲渡または貸与し、もしくは第三者と共用することはできません。

3. 当社は、ユーザーIDとパスワードの組み合わせが登録情報と一致してログインされた場合には、そのユーザーIDを登録しているお客様自身による利用とみなします。

第5条(禁止事項)

お客様は、本サービスの利用にあたり、以下の行為をしてはなりません。

  • 法令または公序良俗に違反する行為
  • 犯罪行為に関連する行為
  • 本サービスの内容等、本サービスに含まれる著作権、商標権ほか知的財産権を侵害する行為
  • 当社、ほかのお客様、またはその他第三者のサーバーまたはネットワークの機能を破壊したり、妨害したりする行為
  • 本サービスによって得られた情報を商業的に利用する行為
  • 当社のサービスの運営を妨害するおそれのある行為
  • 不正アクセスをし、またはこれを試みる行為
  • 他のお客様に関する個人情報等を収集または蓄積する行為
  • 不正な目的を持って本サービスを利用する行為
  • 本サービスの他のお客様またはその他の第三者に不利益、損害、不快感を与える行為
  • 他のお客様に成りすます行為
  • 当社が許諾しない本サービス上での宣伝、広告、勧誘、または営業行為
  • 面識のない異性との出会いを目的とした行為
  • 当社のサービスに関連して、反社会的勢力に対して直接または間接に利益を供与する行為
  • その他、当社が不適切と判断する行為

制定日:2024年1月1日

最終改訂日:2026/7/21