メインコンテンツへスキップ
← 記事一覧に戻る
·収益化·12 min read

個人開発SaaSのトライアル期間設計2026 — Stripe Trial × Supabase で14日無料トライアルを仕組み化する

個人開発SaaSStripeSupabase収益化

結論:4ファイルで14日無料トライアルが動く

ファイル役割
app/api/subscription/start-trial/route.tsトライアル付き Checkout Session を発行
app/api/stripe/webhook/route.tsトライアル開始・終了・課金失敗を Supabase に反映
lib/trial-emails.tsResend で Day0/Day10/Day13 のリマインドメールを送信
supabase/migrations/add_trial_columns.sqltrial_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. 購入障壁が下がる — ¥1,980 を先払いする心理的ハードルを「14日後に判断」に変えられる
  2. 実際の体験で価値を証明できる — ランディングページで説明しきれない体験を届けられる
  3. 解約理由を早期に把握できる — トライアル中の離脱パターンから「価値が伝わらない箇所」を特定できる

一方、設計を誤るとトライアル乱用(フリーローダー問題)が起きます。この記事では「クレジットカード必須のトライアル」を前提に実装します。


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_statusnone / 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つ)

  1. trial_period_days: 14 を Checkout Session に追加して、テストモードで動作確認する
  2. customer.subscription.created webhook ハンドラに trial_ends_at の記録を実装する
  3. Day0 ウェルカムメールを1本書いて Resend で送信テストする

次に読む

Claude Crew Lab Free — 毎月の実験記録をメールで

Claude Code × 個人開発のリアルな事故・発見・SaaS アイデアを毎月第1月曜にお届け。登録で「収益化チェックリスト 15 項目」を無料プレゼント。

個人開発の実験ログを月1回、無料で

失敗 / 実数字 / 仮説 / 次に試すこと。売れた話だけでなく売れなかった理由も共有します。

Next Step

次に読むならこの導線です

すべての記事を見る
有料で次へ進む¥1,000

【第12回】夜寝てる間に Claude Code が記事を書き上げる構成 — 月 ¥5K で動く全コード

Claude Codeラボ全12話の集大成。Skills/MCP/サブエージェント/Hooks/リモート運用を統合した「自走する Claude Crew」を、月 ¥5K の実コストで動かす全構成を公開。寝てる間に競合調査・記事下書き・PR まで自動化する 6 層アーキテクチャの完成版。

次の実験記録も追う

Claude Code × 個人開発の実験ログ、失敗、判断変更をまとめて追いたい人向けに、月次でLab Freeを届けます。

個人開発の実験ログを月1回、無料で

失敗 / 実数字 / 仮説 / 次に試すこと。売れた話だけでなく売れなかった理由も共有します。

この記事が役に立ったらシェア