1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

個人開発SaaSのメール自動化2026 — Resend+Supabase Webhookで登録→有料転換を自動化する実装ガイド

1
Posted at

結論:3通のメール自動化で登録→有料転換を仕組み化できる

Resend + Supabase Database Webhook + Supabase Cron Jobsを使えば、登録直後から6日間のメールシーケンスを全自動化できます。実装工数は約4時間、月固定費は¥0(月3,000通以内)。

完成後の動作フロー:

ユーザー登録(Supabase Auth)
      ↓
Database Webhook → /api/email/welcome
      ↓
Resend API → Day 0 ウェルカムメール
      ↓
email_sequences テーブルに Day3/Day6 予約を記録
      ↓
Supabase Cron(毎時実行)→ 対象レコードを処理
      ↓
Resend API → Day 3 / Day 6 メール送信

Step 1: Resend初期設定 + メールテンプレート

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

const templates: Record<EmailSequenceDay, (name: string) => { subject: string; html: string }> = {
  0: (name) => ({
    subject: `${name}さん、登録ありがとうございます — まず試してほしい3つのこと`,
    html: `
      <p>${name}さん、こんにちは。masatoman です。</p>
      <h2>まず試してほしい3つのこと</h2>
      <ol>
        <li>ダッシュボードからプロジェクトを作成する</li>
        <li>Ep.01「スキル入門」を読む(15分)</li>
        <li>Discordコミュニティに入る</li>
      </ol>
      <p>3日後に「実践でつまずく5つのポイント」をお送りします。</p>
    `,
  }),
  3: (name) => ({
    subject: `${name}さんへ:Claude Codeで時間を削るための3つのコツ`,
    html: `
      <p>${name}さん、登録から3日が経ちました。</p>
      <h2>実践でつまずく3つのポイント</h2>
      <ol>
        <li>CLAUDE.md が育っていない</li>
        <li>コンテキストを使いすぎる(サブエージェント分割でコスト半減)</li>
        <li>Hooks を使っていない</li>
      </ol>
      <p><a href="https://masatoman.net/articles/claude-code-lab-ep01-skills-intro-2026">→ 詳細記事を読む</a></p>
    `,
  }),
  6: (name) => ({
    subject: `${name}さんへ:Standardプランで何が変わるか(正直に話します)`,
    html: `
      <p>${name}さん、登録から6日目です。</p>
      <p>Standardプラン(¥1,980/月)では全エピソードの実装コードをそのまま使えます。</p>
      <p><a href="https://masatoman.net/pricing">→ プランの詳細を見る</a></p>
    `,
  }),
}

export async function sendSequenceEmail({
  to,
  day,
  userName = 'あなた',
}: {
  to: string
  day: EmailSequenceDay
  userName?: string
}) {
  const template = templates[day](userName)
  return resend.emails.send({
    from: 'masatoman <hello@masatoman.net>',
    to,
    subject: template.subject,
    html: template.html,
  })
}

Step 2: 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()
);

-- 重複送信防止
ALTER TABLE email_sequences ADD CONSTRAINT unique_user_day UNIQUE (user_id, day);

-- Cronが高速に未送信レコードを引けるインデックス
CREATE INDEX idx_email_sequences_pending ON email_sequences(send_at)
  WHERE sent_at IS NULL;

Step 3: Webhook受信API(Day 0送信 + Day3/Day6予約)

Supabaseダッシュボード → Database → Webhooks で auth.users の INSERT をトリガーに設定。

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] ?? 'あなた'

  // Day 0: 即時送信
  await sendSequenceEmail({ to: user.email, day: 0, userName })

  // Day 3 / Day 6: 予約記録
  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: 予約メール送信API + Supabase Cron設定

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) => {
      // 楽観的ロック: 二重送信防止
      const { count } = await supabase
        .from('email_sequences')
        .update({ sent_at: new Date().toISOString() })
        .eq('id', mail.id)
        .is('sent_at', null)
        .select('id', { count: 'exact', head: true })

      if (!count) return // 既に別インスタンスが送信済み

      await sendSequenceEmail({
        to: mail.user_email,
        day: mail.day as EmailSequenceDay,
        userName: mail.user_name ?? 'あなた',
      })
    })
  )

  return NextResponse.json({ sent: results.filter((r) => r.status === 'fulfilled').length })
}

Supabase pg_cron設定(毎時実行):

SELECT cron.schedule(
  'send-email-sequences',
  '0 * * * *',
  $$
  SELECT net.http_post(
    url := 'https://your-domain.com/api/email/send-due',
    headers := jsonb_build_object(
      'Authorization', 'Bearer ' || current_setting('app.webhook_secret')
    )
  )
  $$
);

期待できる効果(業界ベンチマーク試算)

SaaS業界の一般的なメールシーケンス効果(Intercom / ChartMogul 等の公開データより):

指標 メール未実装 3通シーケンス実装時
30日以内有料転換率(業界目安) 〜1% 3〜8%
月50人登録時の試算転換人数 約0.5人 1.5〜4人
MRR試算貢献(¥1,980プラン想定) 約¥990 ¥2,970〜¥7,920

※ 上記は業界平均からの試算値です。筆者のプロジェクト(Claude Crew Lab)は現在Free MVPを運用中でベースライン計測中のため、実測値は節目で公開します。

Day 6のプラン比較メールが有効とされる最大の理由は「ROI提示」です。CTAの前に「月¥1,980でClaude Code費用を回収できるか?」という問いを置くことで、読者が自分ゴトとして判断できるようになります。

詳細な実装解説と収益導線の設計は

masatoman.net の有料記事では、リテンションメール(解約予告ユーザー向け)・行動ベーストリガーメールの実装コードも公開しています(¥500)。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?