個人開発SaaSのトライアル期間設計2026 — Stripe Trial × Supabase で14日無料トライアルを仕組み化する
結論:4ファイルで14日無料トライアルが動く
| ファイル | 役割 |
|---|---|
app/api/subscription/start-trial/route.ts | トライアル付き Checkout Session を発行 |
app/api/stripe/webhook/route.ts | トライアル開始・終了・課金失敗を Supabase に反映 |
lib/trial-emails.ts | Resend で Day0/Day10/Day13 のリマインドメールを送信 |
supabase/migrations/add_trial_columns.sql | trial_ends_at / trial_status カラムを追加 |
Stripe に trial_period_days: 14 を渡すだけでトライアルは動きます。難しいのは「トライアルが終わったあと何をするか」の設計です。
masatoman.net では Claude Crew Lab(Free MVP)を運用中です。Standard(¥1,980)・Premium(¥4,980)はローンチ準備段階のため、トライアルの実運用データはまだありません。この記事の実装は Stripe 公式ドキュメントおよびテスト環境での動作確認をベースにしており、業界ベンチマーク(Baremetrics, ChartMogul 等のレポート)を参考に設計しています。
この記事でわかること:
- Stripe でトライアル付きサブスクリプションを作る方法
- トライアル中のユーザーを Supabase で管理するスキーマ設計
- Day0 / Day10 / Day13 のリマインドメール実装
- トライアル終了後の課金失敗(カード未登録)をどう扱うか
- 「で、どう稼ぐ?」— トライアルを転換率に繋げる考え方
なぜトライアルを設計するのか
「無料で試してから決める」は購入者の自然な心理です。SaaS 業界のベンチマーク調査(Baremetrics, OpenView Partners 等)によると、トライアルを提供しているプロダクトは、そうでないプロダクトと比べて初月の転換率が高い傾向があります。ただし、トライアル期間の長さや設計によって効果は大きく変わります。
個人開発 SaaS でトライアルを導入するメリットは次の3点です。
- 購入障壁が下がる — ¥1,980 を先払いする心理的ハードルを「14日後に判断」に変えられる
- 実際の体験で価値を証明できる — ランディングページで説明しきれない体験を届けられる
- 解約理由を早期に把握できる — トライアル中の離脱パターンから「価値が伝わらない箇所」を特定できる
一方、設計を誤るとトライアル乱用(フリーローダー問題)が起きます。この記事では「クレジットカード必須のトライアル」を前提に実装します。
Step 0:Supabase にトライアル管理カラムを追加する
まず subscriptions テーブルにトライアル情報を格納するカラムを追加します。
-- supabase/migrations/add_trial_columns.sql
ALTER TABLE subscriptions
ADD COLUMN IF NOT EXISTS trial_starts_at timestamptz,
ADD COLUMN IF NOT EXISTS trial_ends_at timestamptz,
ADD COLUMN IF NOT EXISTS trial_status text DEFAULT 'none'
CHECK (trial_status IN ('none', 'active', 'converted', 'expired'));
| カラム | 用途 |
|---|---|
trial_starts_at | トライアル開始日時(Stripe webhook で記録) |
trial_ends_at | トライアル終了日時(Stripe webhook で記録) |
trial_status | none / active / converted / expired の4状態 |
Step 1:トライアル付き Checkout Session を発行する
Stripe の Subscription を使うとき、trial_period_days を渡すだけで無料期間が設定されます。
// app/api/subscription/start-trial/route.ts
import { stripe } from '@/lib/stripe'
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
export async function POST(req: Request) {
const supabase = createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const { priceId } = await req.json()
// Stripe Customer を取得または作成
const { data: profile } = await supabase
.from('profiles')
.select('stripe_customer_id, email')
.eq('id', user.id)
.single()
let customerId = profile?.stripe_customer_id
if (!customerId) {
const customer = await stripe.customers.create({ email: profile?.email ?? user.email })
customerId = customer.id
await supabase.from('profiles').update({ stripe_customer_id: customerId }).eq('id', user.id)
}
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
customer: customerId,
line_items: [{ price: priceId, quantity: 1 }],
subscription_data: {
trial_period_days: 14, // ← ここだけで14日トライアルが有効になる
metadata: { user_id: user.id },
},
payment_method_collection: 'always', // カード必須(フリーローダー対策)
success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/dashboard?trial=started`,
cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/pricing`,
})
return NextResponse.json({ url: session.url })
}
payment_method_collection: 'always' がポイントです。カードを登録させることで、トライアル終了と同時に自動課金が走ります。
Step 2:Webhook でトライアル状態を Supabase に反映する
Stripe は以下のイベントを発火します。これらをハンドリングして Supabase を更新します。
| Stripe イベント | タイミング | 対応アクション |
|---|---|---|
customer.subscription.created | トライアル開始時 | trial_status = 'active'、trial_ends_at を記録 |
customer.subscription.updated | トライアル→課金移行時 | trial_status = 'converted' に更新 |
customer.subscription.deleted | トライアル中キャンセル時 | trial_status = 'expired' に更新 |
invoice.payment_failed | 課金失敗時 | ユーザーに支払い更新メールを送信 |
// app/api/stripe/webhook/route.ts(トライアル関連部分)
case 'customer.subscription.created': {
const sub = event.data.object as Stripe.Subscription
const userId = sub.metadata.user_id
if (!userId) break
const trialEnd = sub.trial_end
? new Date(sub.trial_end * 1000).toISOString()
: null
const trialStart = sub.trial_start
? new Date(sub.trial_start * 1000).toISOString()
: null
await supabase.from('subscriptions').upsert({
user_id: userId,
stripe_subscription_id: sub.id,
status: sub.status, // 'trialing'
trial_status: trialEnd ? 'active' : 'none',
trial_starts_at: trialStart,
trial_ends_at: trialEnd,
})
if (trialEnd) {
// Day0 ウェルカムメール
await sendTrialWelcomeEmail(userId, trialEnd)
}
break
}
case 'customer.subscription.updated': {
const sub = event.data.object as Stripe.Subscription
const userId = sub.metadata.user_id
if (!userId) break
// trialing → active への移行 = 転換成功
const isConverted = sub.status === 'active' && !sub.trial_end
await supabase.from('subscriptions').update({
status: sub.status,
trial_status: isConverted ? 'converted' : undefined,
}).eq('stripe_subscription_id', sub.id)
break
}
Step 3:トライアル期間中のリマインドメールを送る
トライアルは放置すると「忘れて期限切れ→解約」になりがちです。3回のメールで体験を後押しします。
// lib/trial-emails.ts
import { Resend } from 'resend'
import { supabaseAdmin } from './supabase/admin'
const resend = new Resend(process.env.RESEND_API_KEY)
export async function sendTrialWelcomeEmail(userId: string, trialEndsAt: string) {
const { data: profile } = await supabaseAdmin
.from('profiles')
.select('email, full_name')
.eq('id', userId)
.single()
if (!profile?.email) return
await resend.emails.send({
from: 'masatoman <hello@masatoman.net>',
to: profile.email,
subject: '14日間のトライアルが始まりました',
html: `
<p>${profile.full_name ?? 'こんにちは'}</p>
<p>トライアル期間は <strong>${new Date(trialEndsAt).toLocaleDateString('ja-JP')}</strong> まで続きます。</p>
<p>まずはダッシュボードを確認してみてください。</p>
<a href="${process.env.NEXT_PUBLIC_SITE_URL}/dashboard">ダッシュボードを開く</a>
`,
})
}
Day10・Day13 のメールは Supabase Edge Functions や Vercel Cron を使って送ります。trial_ends_at が近いユーザーを定期クエリで抽出します。
// app/api/cron/trial-reminder/route.ts
import { createClient } from '@/lib/supabase/server'
import { sendTrialReminderEmail } from '@/lib/trial-emails'
export async function GET() {
const supabase = createClient()
const now = new Date()
// 残り4日以内(Day10の14日トライアルなら10日後に送る)
const fourDaysLater = new Date(now.getTime() + 4 * 24 * 60 * 60 * 1000)
const { data: users } = await supabase
.from('subscriptions')
.select('user_id, trial_ends_at')
.eq('trial_status', 'active')
.lte('trial_ends_at', fourDaysLater.toISOString())
.gte('trial_ends_at', now.toISOString())
for (const user of users ?? []) {
await sendTrialReminderEmail(user.user_id, user.trial_ends_at)
}
return new Response('ok')
}
vercel.json に cron 設定を追加します。
{
"crons": [
{
"path": "/api/cron/trial-reminder",
"schedule": "0 9 * * *"
}
]
}
Step 4:トライアル終了後の課金失敗を扱う
カードを登録していても残高不足や期限切れで課金が失敗することがあります。Stripe は invoice.payment_failed を発火するので、ここでユーザーに通知します。
case 'invoice.payment_failed': {
const invoice = event.data.object as Stripe.Invoice
const customerId = invoice.customer as string
const { data: profile } = await supabaseAdmin
.from('profiles')
.select('id, email')
.eq('stripe_customer_id', customerId)
.single()
if (!profile) break
// Stripe Customer Portal へ誘導
const portalSession = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${process.env.NEXT_PUBLIC_SITE_URL}/dashboard`,
})
await resend.emails.send({
from: 'masatoman <hello@masatoman.net>',
to: profile.email,
subject: '【要確認】お支払いを完了してください',
html: `
<p>お支払いの処理に失敗しました。</p>
<p>カード情報を更新してサービスを継続してください。</p>
<a href="${portalSession.url}">お支払い情報を更新する</a>
`,
})
break
}
トライアル設計の判断ポイント
トライアル期間の長さ
| プロダクト性質 | 推奨期間 |
|---|---|
| セットアップが数分で完了 | 7〜14日 |
| データ蓄積型(効果が出るまで時間がかかる) | 21〜30日 |
| B2B / チーム利用 | 30日以上 |
個人向け SaaS で「14日」が多い理由は、2週間あれば「使う習慣が定着するか」を判断できるからです。
カード必須 vs カード不要
| 方式 | 転換率 | 乱用リスク |
|---|---|---|
| カード必須(opt-out 型) | 高い傾向 | 低い |
| カード不要(opt-in 型) | 低い傾向 | 高い |
個人開発 SaaS では、乱用対策とリソース節約の観点からカード必須を推奨します。
で、どう稼ぐ?
トライアルは「転換装置」です。ただ設置するだけでは意味がなく、3つの設計が揃って初めて機能します。
1. Day0 ウェルカムメールで「最初の成功体験」に誘導する トライアル開始直後に「まずこれをやってみてください」と1アクションを指定します。機能一覧を羅列するのではなく、「この1つをやれば価値がわかる」ポイントに絞ります。
2. Day10 リマインドメールで「使っていない人」を掘り起こす トライアルを申し込んでも半数以上は初日以降ログインしません(業界ベンチマーク参照)。Day10 で「まだ試していない機能」への誘導メールを送ることで、残り4日で体験してもらえる可能性が上がります。
3. Day13「明日終了」メールで意思決定を促す 「明日自動で課金されます。継続しない場合は今すぐキャンセル可能です」という正直な通知です。隠すより誠実に伝えた方が、長期的な解約率が低くなる傾向があります(Stripe の公式ブログでも推奨されている手法です)。
「で、どう稼ぐ?」の本質
トライアルは入口を広くする施策です。しかし、入口だけ広くしても「中で何を体験させるか」が設計できていなければ転換しません。トライアル設計と同時に、Day0〜Day14 の体験シナリオを設計することが、収益につながる本当の仕事です。
今日やること(3つ)
trial_period_days: 14を Checkout Session に追加して、テストモードで動作確認するcustomer.subscription.createdwebhook ハンドラにtrial_ends_atの記録を実装する- Day0 ウェルカムメールを1本書いて Resend で送信テストする
次に読む
- 個人開発SaaSのダニング管理2026 — 課金失敗後のリカバリフローを詳しく解説
- 個人開発SaaSの年払いプラン追加2026 — トライアル後に年払いへ誘導するパターン
- 個人開発SaaSのプロモーションコード設計2026 — トライアルと組み合わせた割引施策
Claude Crew Lab Free — 毎月の実験記録をメールで
Claude Code × 個人開発のリアルな事故・発見・SaaS アイデアを毎月第1月曜にお届け。登録で「収益化チェックリスト 15 項目」を無料プレゼント。
個人開発の実験ログを月1回、無料で
失敗 / 実数字 / 仮説 / 次に試すこと。売れた話だけでなく売れなかった理由も共有します。
Next Step
次に読むならこの導線です
【第12回】夜寝てる間に Claude Code が記事を書き上げる構成 — 月 ¥5K で動く全コード
Claude Codeラボ全12話の集大成。Skills/MCP/サブエージェント/Hooks/リモート運用を統合した「自走する Claude Crew」を、月 ¥5K の実コストで動かす全構成を公開。寝てる間に競合調査・記事下書き・PR まで自動化する 6 層アーキテクチャの完成版。
【第6回】Claude Code で SaaS を 1 週間で組む開発フロー — Skill × MCP の統合設計
Skills、MCP、サブエージェント、Hooks を統合した個人開発の SaaS 構築フローを解説。LP 公開から課金開始までを 1 週間で組み立てるパイプラインの設計と実装を、月 500 万トークン運用の筆者が公開。「売れるかどうか」は別途検証が必要なので、本記事は「作る速度を上げる」フローに特化しています。
【第10回】Claude Code × Supabase で管理画面を30分で生成する Skill 実装ガイド
Supabase のテーブルを参照して管理画面を自動生成する Skill を、実コード付きで解説。Claude Code が DB スキーマから Next.js 管理画面を30分で生成する仕組みを公開します。
【第12回】夜寝てる間に Claude Code が記事を書き上げる構成 — 月 ¥5K で動く全コード
Claude Codeラボ全12話の集大成。Skills/MCP/サブエージェント/Hooks/リモート運用を統合した「自走する Claude Crew」を、月 ¥5K の実コストで動かす全構成を公開。寝てる間に競合調査・記事下書き・PR まで自動化する 6 層アーキテクチャの完成版。
次の実験記録も追う
Claude Code × 個人開発の実験ログ、失敗、判断変更をまとめて追いたい人向けに、月次でLab Freeを届けます。
個人開発の実験ログを月1回、無料で
失敗 / 実数字 / 仮説 / 次に試すこと。売れた話だけでなく売れなかった理由も共有します。
この記事が役に立ったらシェア