個人開発SaaSのFree→有料転換トリガー設計2026 — 「制限の壁」でなく「価値の予告」で転換率を上げる実装ガイド
結論:転換トリガーは「制限の壁」ではなく「価値の予告」を起点にする
Free→有料転換の設計で最も多い失敗は、機能を制限してユーザーを強制的にアップグレード画面へ誘導するパターンです。壁にぶつかったユーザーが感じるのは「買いたい」ではなく「邪魔された」という不快感です。
実際に転換が起きる瞬間は決まっています。ユーザーが「これで解決できる」と実感した直後です。その瞬間にアップグレードの選択肢を出すのが、最も摩擦が少なく転換率が高い設計です。
具体的には3つのトリガーを組み合わせます:
- 価値実感ポイント後のアップグレード提示(AHAモーメントの翌日)
- 使用量上限の「予告」通知(実際にブロックする前に出す)
- 「もっと使いたい」シグナルの自動検知(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)
重要なのは、転換率は「制限の強さ」ではなく「価値の見せ方」で変わるという点です。ユーザーが「これは自分に必要だ」と感じた瞬間に選択肢を提示できれば、強制的な制限は必要ありません。
個人開発での実装優先順位
- まず AHAモーメントを定義する(何をしたら「価値を感じた」と言えるか)
- PostHog でそのイベントを計測し始める
- 転換トリガーテーブルを作り、メールを1種類送る
- ファネルを計測して離脱ポイントを特定する
- 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回、無料で
失敗 / 実数字 / 仮説 / 次に試すこと。売れた話だけでなく売れなかった理由も共有します。
関連記事
- 個人開発SaaSのオンボーディング設計2026 — 本記事のAHAモーメント起点となるオンボーディング実装
- Stripe Webhook × Supabase でチャーン検知を自動化する2026 — 転換後のチャーン対策
- 個人開発SaaSのメール自動化2026 — Resend連携の基礎実装
この記事が役に立ったらシェア