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

個人開発SaaSのFree→有料転換トリガー設計2026 — 「制限の壁」でなく「価値の予告」で転換率を上げる実装ガイド

個人開発SaaSSupabasePostHog収益化

結論:転換トリガーは「制限の壁」ではなく「価値の予告」を起点にする

Free→有料転換の設計で最も多い失敗は、機能を制限してユーザーを強制的にアップグレード画面へ誘導するパターンです。壁にぶつかったユーザーが感じるのは「買いたい」ではなく「邪魔された」という不快感です。

実際に転換が起きる瞬間は決まっています。ユーザーが「これで解決できる」と実感した直後です。その瞬間にアップグレードの選択肢を出すのが、最も摩擦が少なく転換率が高い設計です。

具体的には3つのトリガーを組み合わせます:

  1. 価値実感ポイント後のアップグレード提示(AHAモーメントの翌日)
  2. 使用量上限の「予告」通知(実際にブロックする前に出す)
  3. 「もっと使いたい」シグナルの自動検知(PostHogでログ→Supabaseで閾値管理)

masatoman.net では記事80本以上を公開し、Claude Crew Lab(Free MVP)を運用中です。Standard(¥1,980)・Premium(¥4,980)はローンチ準備段階のため、Free→有料転換率の実測値は現時点では公開できる段階にありません。この記事で紹介するトリガー設計は、SaaS業界のベンチマーク(Intercom・ChartMogul・Baremetrics等の公開レポート)と、筆者が Next.js + Supabase + PostHog で実際に実装・動作確認した実装パターンをベースにしています。


なぜ「制限の壁」だけでは転換が起きないのか

痛みベースのアップグレード誘導の問題

機能を止めてから「有料にしてください」と表示する方式は、3つの問題を抱えています。

① 離脱率が上がる:制限に引っかかった時点でユーザーはまだ「このプロダクトで課題を解決できる」と確信していません。確信がない状態で課金を求めるため、そのままアプリを閉じる比率が高くなります。

② サポートコストが上がる:「なぜ使えなくなったのか」という問い合わせが発生します。個人開発では対応コストが直接時間コストになります。

③ クチコミがネガティブになる:「機能制限がうざい」という体験はSNSでシェアされやすい感情です。制限系の不満は拡散されます。

転換が自然に起きる瞬間

業界調査(Intercom社「State of Customer Engagement 2024」)によれば、SaaS製品でアップグレードが起きやすいタイミングは:

  • プロダクトで最初に「期待通りの結果」を得た直後
  • 複数の機能を横断して使い始めたタイミング
  • 7日以内に一定回数以上セッションしたユーザー

共通しているのは「すでに価値を実感している」状態であることです。この状態でアップグレードを提案するから転換が起きます。


トリガー設計の全体像

ユーザー登録
    ↓
onboarding_steps 完了(3ステップ)
    ↓  ← ここが「価値実感ポイント」
PostHog capture('aha_moment_reached')
    ↓
Supabase: conversion_triggers に記録
    ↓
翌日: アップグレード提案メール(Resend)
+ ダッシュボードにバナー表示

前回のオンボーディング設計記事で実装した OnboardingChecklist の完了イベントを起点にします。


ステップ1:conversion_triggers テーブルを作成する

CREATE TABLE conversion_triggers (
  id          uuid DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id     uuid REFERENCES auth.users(id) ON DELETE CASCADE,
  trigger_type text NOT NULL,
  triggered_at timestamptz DEFAULT NOW(),
  email_sent  boolean DEFAULT false,
  converted   boolean DEFAULT false,
  UNIQUE(user_id, trigger_type)
);

ALTER TABLE conversion_triggers ENABLE ROW LEVEL SECURITY;
CREATE POLICY "users_own_triggers" ON conversion_triggers
  FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "service_role_insert" ON conversion_triggers
  FOR INSERT WITH CHECK (auth.role() = 'service_role');

trigger_type に記録するのは以下の3種類です:

トリガー種別発火条件
aha_momentオンボーディング3ステップ完了
usage_warning月次使用量が上限の70%に到達
heavy_user登録7日以内に10セッション以上

ステップ2:AHAモーメント検知とPostHogイベント送信

前回記事の OnboardingChecklist に追記します。

// src/components/onboarding-checklist.tsx(追記部分)

const toggle = async (step: string) => {
  const next = !completed[step]
  await supabase.from('onboarding_steps').upsert({
    user_id: userId,
    step,
    completed: next,
    updated_at: new Date().toISOString(),
  })
  setCompleted((prev) => ({ ...prev, [step]: next }))

  if (next) {
    const updatedCompleted = { ...completed, [step]: true }
    const allDone = STEPS.every((s) => updatedCompleted[s.key])

    posthog.capture('onboarding_step_completed', { step })

    if (allDone) {
      posthog.capture('aha_moment_reached', {
        user_id: userId,
        completed_at: new Date().toISOString(),
      })
      await fetch('/api/conversion-trigger', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ triggerType: 'aha_moment' }),
      })
    }
  }
}

ステップ3:API Route でトリガーを記録する

// app/api/conversion-trigger/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { createClient as createAdmin } from '@supabase/supabase-js'

const adminSupabase = createAdmin(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

export async function POST(req: NextRequest) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

  const { triggerType } = await req.json()

  const { error } = await adminSupabase.from('conversion_triggers').upsert({
    user_id: user.id,
    trigger_type: triggerType,
    triggered_at: new Date().toISOString(),
  }, { onConflict: 'user_id,trigger_type' })

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }

  return NextResponse.json({ ok: true })
}

ステップ4:「予告」ベースの使用量警告を実装する

制限に達する前に出すのがポイントです。上限の70%で通知します。

// lib/usage-check.ts
import { createClient } from '@supabase/supabase-js'

const MONTHLY_LIMIT = 100
const WARNING_THRESHOLD = 0.7

export async function checkAndRecordUsageWarning(userId: string, currentUsage: number) {
  if (currentUsage / MONTHLY_LIMIT < WARNING_THRESHOLD) return

  const adminSupabase = createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.SUPABASE_SERVICE_ROLE_KEY!
  )

  await adminSupabase.from('conversion_triggers').upsert({
    user_id: userId,
    trigger_type: 'usage_warning',
    triggered_at: new Date().toISOString(),
  }, { onConflict: 'user_id,trigger_type' })
}

ダッシュボードでの表示例:

// src/components/usage-warning-banner.tsx
'use client'

import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'

export function UsageWarningBanner({ userId }: { userId: string }) {
  const [show, setShow] = useState(false)
  const supabase = createClient()

  useEffect(() => {
    supabase
      .from('conversion_triggers')
      .select('trigger_type')
      .eq('user_id', userId)
      .eq('trigger_type', 'usage_warning')
      .single()
      .then(({ data }) => {
        if (data) setShow(true)
      })
  }, [userId, supabase])

  if (!show) return null

  return (
    <div className="rounded-md bg-amber-50 border border-amber-200 px-4 py-3 text-sm">
      <p className="font-medium text-amber-800">今月の使用量が70%に達しました</p>
      <p className="mt-1 text-amber-700">
        上限に達する前に、
        <a href="/pricing" className="underline font-medium">Standardプラン</a>
        へのアップグレードをご検討ください。
      </p>
    </div>
  )
}

ステップ5:アップグレード提案メールを自動送信する

トリガー記録から24時間後にメールを送る Supabase Edge Function を作ります。

// supabase/functions/send-upgrade-email/index.ts
import { createClient } from '@supabase/supabase-js'
import { Resend } from 'npm:resend'

const supabase = createClient(
  Deno.env.get('SUPABASE_URL')!,
  Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
const resend = new Resend(Deno.env.get('RESEND_API_KEY')!)

Deno.serve(async () => {
  const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()

  const { data: triggers } = await supabase
    .from('conversion_triggers')
    .select('user_id, trigger_type')
    .eq('email_sent', false)
    .lte('triggered_at', cutoff)

  if (!triggers || triggers.length === 0) return new Response('no pending triggers')

  for (const trigger of triggers) {
    const { data: profile } = await supabase
      .from('profiles')
      .select('email, name')
      .eq('id', trigger.user_id)
      .single()

    if (!profile) continue

    const subject = trigger.trigger_type === 'usage_warning'
      ? '使用量が70%に達しました — Standard プランで続けませんか'
      : `${profile.name}さん、次のステップへ進みませんか`

    await resend.emails.send({
      from: 'masato@masatoman.net',
      to: profile.email,
      subject,
      text: `${profile.name} さん\n\nご利用ありがとうございます。\n\nStandard プラン(¥1,980/月)では、制限なくすべての機能をご利用いただけます。\n\nhttps://masatoman.net/pricing\n\nmasato`,
    })

    await supabase
      .from('conversion_triggers')
      .update({ email_sent: true })
      .eq('user_id', trigger.user_id)
      .eq('trigger_type', trigger.trigger_type)
  }

  return new Response(`sent: ${triggers.length}`)
})

Edge Function の定期実行(毎朝9時)は Supabase の cron 設定で行います。

SELECT cron.schedule(
  'send-upgrade-emails',
  '0 9 * * *',
  $$SELECT net.http_post(
    url := 'https://<project>.supabase.co/functions/v1/send-upgrade-email',
    headers := '{"Authorization": "Bearer <anon-key>"}'::jsonb
  )$$
);

ステップ6:PostHogでトリガー〜転換のファネルを計測する

aha_moment_reached
→ upgrade_page_viewed
→ checkout_started
→ subscription_created

PostHog の「Funnels」でこの4ステップを設定すると、どこで離脱しているかが可視化できます。

計測ポイントのコード:

// 価格ページを開いた時
posthog.capture('upgrade_page_viewed', { trigger_type: 'aha_moment' })

// チェックアウトを開始した時
posthog.capture('checkout_started', { plan: 'standard', price: 1980 })

ファネルのどのステップで離脱が多いかによって、次の改善アクションが変わります:

離脱ステップ考えられる原因改善アクション
upgrade_page_viewed の前メールが届いていない or スルーされた件名A/Bテストまたはタイミング変更
checkout_started の前価格ページの説明が不十分Free/Standard の比較表を改善
subscription_created の前決済フローの摩擦Stripe の決済UIを確認

で、どう稼ぐ?

転換トリガー設計が「月5万円の収益構造」を作る

シンプルな試算を示します(業界ベンチマーク水準の想定値です。筆者の実測値ではありません)。

フェーズFree登録者転換率(業界水準3〜8%)Standard収益
試算例A(低め)100人3% = 3人¥5,940/月
試算例B(中央)100人5% = 5人¥9,900/月
試算例C(高め)100人8% = 8人¥15,840/月

(転換率3〜8%は SaaS業界の一般的水準。出典: ChartMogul SaaS Benchmarks 2024)

重要なのは、転換率は「制限の強さ」ではなく「価値の見せ方」で変わるという点です。ユーザーが「これは自分に必要だ」と感じた瞬間に選択肢を提示できれば、強制的な制限は必要ありません。

個人開発での実装優先順位

  1. まず AHAモーメントを定義する(何をしたら「価値を感じた」と言えるか)
  2. PostHog でそのイベントを計測し始める
  3. 転換トリガーテーブルを作り、メールを1種類送る
  4. ファネルを計測して離脱ポイントを特定する
  5. 2〜3ヶ月のデータを見てから最適化する

AHAモーメントの定義ができていないまま転換施策を打ってもデータが散らばります。まずステップ1の「どのイベントがAHAか」を決めることから始めてください。

Claude Crew Lab との連携

Claude Crew Lab(Free)では、この記事で紹介した PostHog イベント設計や Supabase trigger テーブルの構成について、実際の設定レビューや詰まりポイントのサポートを行っています。

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

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

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

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


関連記事

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