結論:3ファイルでオンボーディングの骨格が完成する
Supabaseに onboarding_steps テーブルを追加し、Next.js にチェックリストコンポーネントを置き、Resend でDay 0/3/6/14のメールシーケンスを動かす——これがオンボーディング実装の全体像です。
// ダッシュボードでの使用例
<OnboardingChecklist userId={session.user.id} />
この記事では Next.js App Router + Supabase + Resend + PostHog の実装を解説します。
必要なパッケージ
npm install resend posthog-js posthog-node
onboarding_stepsテーブル
CREATE TABLE onboarding_steps (
user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE,
step text NOT NULL,
completed boolean DEFAULT false,
updated_at timestamptz DEFAULT NOW(),
PRIMARY KEY (user_id, step)
);
チェックリストコンポーネント
src/components/onboarding-checklist.tsx:
'use client'
import { createClient } from '@/lib/supabase/client'
import { usePostHog } from 'posthog-js/react'
import { useEffect, useState } from 'react'
const STEPS = [
{ key: 'profile_set', label: 'プロフィールを設定する' },
{ key: 'first_content', label: '最初のコンテンツを読む' },
{ key: 'dashboard_visit', label: 'ダッシュボードを確認する' },
]
export function OnboardingChecklist({ userId }: { userId: string }) {
const supabase = createClient()
const posthog = usePostHog()
const [completed, setCompleted] = useState<Record<string, boolean>>({})
useEffect(() => {
supabase
.from('onboarding_steps')
.select('step, completed')
.eq('user_id', userId)
.then(({ data }) => {
if (!data) return
const map: Record<string, boolean> = {}
data.forEach((row) => { map[row.step] = row.completed })
setCompleted(map)
})
}, [userId, supabase])
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 doneCount = Object.values({ ...completed, [step]: true }).filter(Boolean).length
posthog.capture('onboarding_step_completed', {
step,
total_completed: doneCount,
total_steps: STEPS.length,
})
}
}
const doneCount = Object.values(completed).filter(Boolean).length
const pct = Math.round((doneCount / STEPS.length) * 100)
return (
<div className="rounded-lg border p-4 space-y-3">
<div className="flex items-center justify-between">
<p className="font-medium text-sm">スタートガイド</p>
<span className="text-xs text-muted-foreground">{pct}% 完了</span>
</div>
<div className="w-full bg-muted rounded-full h-1.5">
<div
className="bg-primary h-1.5 rounded-full transition-all"
style={{ width: `${pct}%` }}
/>
</div>
<ul className="space-y-2">
{STEPS.map(({ key, label }) => (
<li key={key} className="flex items-center gap-2 text-sm">
<button
onClick={() => toggle(key)}
className={`w-4 h-4 rounded border flex-shrink-0 transition-colors ${
completed[key]
? 'bg-primary border-primary'
: 'border-muted-foreground'
}`}
aria-label={label}
/>
<span className={completed[key] ? 'line-through text-muted-foreground' : ''}>
{label}
</span>
</li>
))}
</ul>
</div>
)
}
7日間メールシーケンスの拡張(Day 14追加)
前回のメール自動化実装から拡張して、Day 14の再活性化メールを追加します。
// src/lib/resend.ts の templates に追加
14: (name) => ({
subject: `${name}さん、最近いかがですか?`,
html: `<p>登録から2週間が経ちました。</p>
<p>まだ試せていない機能がありましたら、お気軽にご連絡ください。</p>
<p><a href="https://masatoman.net/dashboard">ダッシュボードを開く →</a></p>`,
}),
-- Webhook受信時にDay 14も予約
INSERT INTO email_sequences (user_id, user_email, user_name, day, send_at)
VALUES ($1, $2, $3, 14, NOW() + INTERVAL '14 days')
ON CONFLICT (user_id, day) DO NOTHING;
PostHogでオンボーディング完了率を計測する
Funnels に以下を設定します:
onboarding_step_completed (profile_set)
→ onboarding_step_completed (first_content)
→ onboarding_step_completed (dashboard_visit)
→ subscription_created
このファネルでオンボーディング完了者と未完了者の転換率差を可視化できます。
設計思想・Aha!moment定義・収益導線の詳細解説はmasatoman.net の記事で公開しています。