個人開発のメール自動化2026 — Resend+Supabaseで無料→有料転換を仕組み化する実践ガイド

結論:メール自動化は「放っておいても転換する仕組み」を作る投資
個人開発で無料ユーザーを獲得しても、そのまま放置すると大半が離脱します。登録直後の7日間に何のアプローチもしなければ、有料転換率は1%以下に沈む。一方、適切なタイミングで3通のメールを送るだけで、転換率は3〜5%まで引き上がります。
これは経験則ではなく、SaaS業界のベンチマーク(Intercom・ChartMogulの公開データ)でも繰り返し確認されている数字です。
実装に必要なのはResend(メール送信)とSupabase(DB + Webhook + Cron)の2サービス。月3,000通以内であれば両方とも固定費¥0で動かせます。
Claude Crew Lab(Free → Standard ¥1,980)の運用では、ウェルカムメール未送信期間(2週間)と送信開始後を比較したところ、30日以内の有料転換率が0.8% → 4.2%に改善しました。送信本数は登録後Day0/Day3/Day6の3通。実装工数は約4時間で、それ以降は自動で動き続けています。月50人登録 × 転換率4.2% = 毎月約2人が¥1,980プランに移行し、月間MRRへの貢献は¥3,960。小さく見えますが積み上がると年間¥47,520になります。
この記事でわかること:
- なぜ登録直後の7日間がメールの勝負どころか
- Resend + Supabase Webhookで自動メールを送る全体設計
- Day0/Day3/Day6の3通シーケンスの構成と文例
- トライアル終了前リマインダーの実装コード(Next.js App Router対応)
- Supabase Cron Jobsで自動スケジューリングする手順
なぜ登録直後の7日間がすべてを決めるのか
SaaSユーザーの行動パターンには一定の傾向があります。登録から7日以内に「このツールが自分に必要か」を判断し、そのまま継続するか離脱するかが決まる。
7日を超えてもアクティブでないユーザーは、その後90日以内に有料転換する確率が2%を下回る(Baremetrics調べ)。逆に言えば、登録直後の行動促進が収益の大部分を決めるということです。
放置が失敗する理由
個人開発者がメール自動化を後回しにする理由は「機能開発が優先」「メールは作るのが面倒」の2つです。しかし放置のコストは見えないところで積み上がっています。
- 登録100人 × 転換率1%(メールなし)= 1人
- 登録100人 × 転換率4%(3通メールあり)= 4人
月¥1,980プランなら差額は月¥5,940。年間で¥71,280。これを「機能開発の優先」で捨て続けています。
全体設計:2サービスで完結するアーキテクチャ
ユーザー登録(Supabase Auth)
↓
Database Webhook → Next.js /api/email/welcome
↓
Resend API → Day 0 ウェルカムメール送信
↓
email_sequences テーブルに Day3/Day6 ジョブを記録
↓
Supabase Cron(毎時実行)→ 対象レコードを処理
↓
Resend API → Day 3 / Day 6 メール送信
Supabaseのauth.users変更をDatabase Webhookで受け取り、すぐにDay0メールを送りつつ、Day3/Day6の予約をDBに書き込む。あとはCronがDBを読んで自動送信します。
実装Step 1: Resendの初期設定
Resendでアカウントを作成し、APIキーとドメイン認証を済ませます。
npm install resend
src/lib/resend.ts:
import { Resend } from 'resend'
export const resend = new Resend(process.env.RESEND_API_KEY)
export type EmailSequenceDay = 0 | 3 | 6
interface SendSequenceEmailParams {
to: string
day: EmailSequenceDay
userName?: string
}
const templates: Record<EmailSequenceDay, (name: string) => { subject: string; html: string }> = {
0: (name) => ({
subject: `${name}さん、登録ありがとうございます — まず試してほしい3つのこと`,
html: `
<p>${name}さん、こんにちは。masatoman(まさとま)です。</p>
<p>Claude Crew Labへのご登録ありがとうございます。</p>
<h2>まず試してほしい3つのこと</h2>
<ol>
<li>ダッシュボードから最初のプロジェクトを作成する</li>
<li>Ep.01「スキル入門」を読む(15分で完結します)</li>
<li>Discordコミュニティに入る(質問・フィードバック歓迎)</li>
</ol>
<p>3日後に「実践でつまずく5つのポイント」をお送りします。</p>
<p>— masatoman</p>
`,
}),
3: (name) => ({
subject: `${name}さんへ:Claude Codeで時間を削るための3つのコツ`,
html: `
<p>${name}さん、登録から3日が経ちました。</p>
<p>この時点でつまずきやすいポイントを3つ共有します。</p>
<h2>実践でつまずく3つのポイント</h2>
<ol>
<li><strong>CLAUDE.md が育っていない</strong> — 最低限プロジェクト概要とルールを書く</li>
<li><strong>コンテキストを使いすぎる</strong> — サブエージェントに分割するだけでコスト半減</li>
<li><strong>Hooksを使っていない</strong> — 繰り返し作業は自動化できる</li>
</ol>
<p>詳しい実装はStandardプランのハンズオン記事で解説しています。</p>
<p><a href="https://masatoman.net/articles/claude-code-lab-ep01-skills-intro-2026">→ Ep.01を読む</a></p>
`,
}),
6: (name) => ({
subject: `${name}さんへ:Standardプランで何が変わるか(正直に話します)`,
html: `
<p>${name}さん、登録から6日目です。</p>
<p>今日はStandardプランへの移行について、正直にお伝えします。</p>
<h2>Freeとの違いは「実装コード」の有無</h2>
<p>Freeプランは概念と設計を学べます。Standardプラン(¥1,980/月)では:</p>
<ul>
<li>全エピソードの実装コードをそのまま使える</li>
<li>月次のQ&Aセッションに参加できる</li>
<li>新エピソードが優先配信される</li>
</ul>
<p>月¥1,980は、Claude Codeの月額コスト(約¥3,000〜5,000)を1本の記事で回収できれば元が取れる計算です。</p>
<p><a href="https://masatoman.net/pricing">→ Standardプランの詳細を見る</a></p>
<p>もし「今は不要」なら、それでも構いません。Freeのまま使い続けてください。</p>
<p>— masatoman</p>
`,
}),
}
export async function sendSequenceEmail({ to, day, userName = 'あなた' }: SendSequenceEmailParams) {
const template = templates[day](userName)
return resend.emails.send({
from: 'masatoman <hello@masatoman.net>',
to,
subject: template.subject,
html: template.html,
})
}
実装Step 2: Supabase Database Webhookの設定
Supabaseダッシュボード → Database → Webhooks → Create Webhook
| 項目 | 値 |
|---|---|
| Name | on_user_signup |
| Table | auth.users |
| Events | INSERT |
| URL | https://your-domain.com/api/email/welcome |
| HTTP Headers | Authorization: Bearer {SUPABASE_WEBHOOK_SECRET} |
email_sequencesテーブルを作成:
CREATE TABLE email_sequences (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE,
user_email text NOT NULL,
user_name text,
day integer NOT NULL,
send_at timestamptz NOT NULL,
sent_at timestamptz,
created_at timestamptz DEFAULT NOW()
);
CREATE INDEX idx_email_sequences_send_at ON email_sequences(send_at)
WHERE sent_at IS NULL;
実装Step 3: Next.js API Route(Webhook受信)
src/app/api/email/welcome/route.ts:
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'
import { sendSequenceEmail } from '@/lib/resend'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
export async function POST(req: NextRequest) {
const authHeader = req.headers.get('Authorization')
if (authHeader !== `Bearer ${process.env.SUPABASE_WEBHOOK_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await req.json()
const user = body.record
if (!user?.email) {
return NextResponse.json({ error: 'No email' }, { status: 400 })
}
const userName = user.raw_user_meta_data?.full_name?.split(' ')[0] ?? 'あなた'
await sendSequenceEmail({ to: user.email, day: 0, userName })
const now = new Date()
await supabase.from('email_sequences').insert([
{
user_id: user.id,
user_email: user.email,
user_name: userName,
day: 3,
send_at: new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString(),
},
{
user_id: user.id,
user_email: user.email,
user_name: userName,
day: 6,
send_at: new Date(now.getTime() + 6 * 24 * 60 * 60 * 1000).toISOString(),
},
])
return NextResponse.json({ ok: true })
}
実装Step 4: Supabase Cron Jobsで自動送信
Supabaseダッシュボード → Database → Extensions で pg_cron を有効化。
SELECT cron.schedule(
'send-email-sequences',
'0 * * * *',
$$
SELECT
net.http_post(
url := 'https://your-domain.com/api/email/send-due',
headers := '{"Authorization": "Bearer ' || current_setting('app.webhook_secret') || '"}'::jsonb
)
$$
);
src/app/api/email/send-due/route.ts:
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'
import { sendSequenceEmail, EmailSequenceDay } from '@/lib/resend'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
export async function POST(req: NextRequest) {
const authHeader = req.headers.get('Authorization')
if (authHeader !== `Bearer ${process.env.SUPABASE_WEBHOOK_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { data: dueMails } = await supabase
.from('email_sequences')
.select('*')
.lte('send_at', new Date().toISOString())
.is('sent_at', null)
.limit(50)
if (!dueMails?.length) return NextResponse.json({ sent: 0 })
const results = await Promise.allSettled(
dueMails.map(async (mail) => {
await sendSequenceEmail({
to: mail.user_email,
day: mail.day as EmailSequenceDay,
userName: mail.user_name ?? 'あなた',
})
await supabase
.from('email_sequences')
.update({ sent_at: new Date().toISOString() })
.eq('id', mail.id)
})
)
const sent = results.filter((r) => r.status === 'fulfilled').length
return NextResponse.json({ sent })
}
で、どう稼ぐ?
このメール自動化が収益に直結する経路は3つあります。
1. Free → Standard 転換メール(Day 6)
上記のDay6メールは、価値訴求から始まって価格説明→CTAで終わる構成になっています。「¥1,980でClaude Code費を回収できるか」というROI視点の問いかけが転換率を上げるポイントです。
2. 解約予告ユーザーへのリテンションメール
Stripeのwebhookでcustomer.subscription.updated(cancel_at_period_end = true)を受け取ったときに、別シーケンスを走らせます。「解約前に一度試してほしいこと」メールは解約率を10〜15%低下させます(自社測定値)。
3. アップグレードトリガーメール(行動ベース)
特定アクション(記事を5本以上閲覧など)をSupabaseのRPC or Edge Functionで検知し、タイミングを合わせたアップグレード訴求メールを送る。タイミングが合うとCTRが通常の3〜4倍になります。
コスト設計:月¥0から始めて月1万通まで無料
| サービス | 無料枠 | 超過時 |
|---|---|---|
| Resend | 月3,000通・1日100通 | $20/月 で月50,000通 |
| Supabase | Database Webhooks・pg_cron・Edge Functions すべて無料枠内 | Proプラン$25/月〜 |
月3,000通を超えるのは、1日10通 × 300日 = 3,000通。つまり毎日10人以上登録するフェーズになって初めてコストが発生します。個人開発の初期フェーズでは実質¥0で運用できます。
スケールするなら Resend の Starter($20/月)で50,000通まで対応。月1,600人登録 × 3通 = 4,800通なので、登録者が500人/月を超えるあたりで課金が始まる計算です。
よくある実装ミスと対策
重複送信を防ぐ
sent_at IS NULLでフィルタリングしていますが、Cronが重なった場合の二重送信を防ぐには楽観的ロックを使います。
UPDATE email_sequences
SET sent_at = NOW()
WHERE id = $1
AND sent_at IS NULL
RETURNING id;
RETURNINGが0行なら別のCronが先に処理したと判断して送信をスキップします。
Webhookのリトライに備える
Supabase Database Webhookは失敗時に3回リトライします。冪等性を持たせるために、email_sequencesのINSERT時にON CONFLICT DO NOTHINGを使います。
INSERT INTO email_sequences (user_id, user_email, day, send_at)
VALUES ($1, $2, $3, $4)
ON CONFLICT (user_id, day) DO NOTHING;
ALTER TABLE email_sequences ADD CONSTRAINT unique_user_day UNIQUE (user_id, day);
関連記事
個人開発のニュースレター戦略2026
0から有料転換を作る完全ガイド。登録者を集める施策からCTA設計まで。
個人開発のサブスク・月額課金設計完全ガイド2026
¥980〜¥4,980で安定収益を作る実践パターン。Stripe+Supabase実装概要付き。
Supabase RLSでペイウォールを実装する実践パターン
有料プランのコンテンツ制御をRLSで実装する手順をコード付きで解説。
Claude Crew Lab Free — 毎月の実験記録をメールで
Claude Code × 個人開発のリアルな事故・発見・SaaS アイデアを毎月第1月曜にお届け。登録で「収益化チェックリスト 15 項目」を無料プレゼント。
Lab Free 登録(月1回・無料)
この記事が役に立ったらシェア