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

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

個人開発メール自動化ResendSupabaseSaaS
個人開発のメール自動化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

項目
Nameon_user_signup
Tableauth.users
EventsINSERT
URLhttps://your-domain.com/api/email/welcome
HTTP HeadersAuthorization: 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通
SupabaseDatabase 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回・無料)

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