サブスクリプション課金は、「毎月同じ処理を自動で繰り返す」という一点だけで、単発の決済とは設計の難易度が変わります。本記事では、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層で、最小構成は完結します。
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の会員制サイト構築事例の構成がそのまま流用できます。
