Back to Blog
從零打造 EDM 系統(一):整體架構設計與技術選型
📝 Dev Notes

從零打造 EDM 系統(一):整體架構設計與技術選型

B
Blake
Dec 12, 2025 By Blake 17 min read
身為個人品牌經營者或小型團隊,電子報是維繫讀者關係的重要工具。市面上有 Mailchimp、ConvertKit、Substack 等成熟服務,但當你需要更多客製化、想控制成本、或單純想深入了解系統如何運作時,自建 EDM 系統就成了有趣的選項。 這系列文章將分享我從零開始打造 EDM 系統的完整歷程,包含架構設計、API 串接、事件追蹤到營運優化的所有細節。

為什麼要自建 EDM 系統?

在開始之前,先釐清自建的合理時機:

✅ 適合自建的情況

  • 訂閱者數量適中(500~10,000 人)但需要高度客製化

  • 整合自有系統(CRM、會員資料庫、內容管理系統等)

  • 掌控數據隱私,完全自有不依賴第三方

  • 學習需求,想深入理解 Email 發送的技術細節

  • 成本最優,免費額度 + 按量計費相比訂閱制更划算

❌ 不適合自建的情況

  • 需要複雜的自動化行銷流程(Drip Campaign、條件分支等)

  • 缺乏工程資源維護和迭代系統

  • 訂閱者數量龐大(>50,000),需要企業級支援和 SLA 保障

  • 需要高度複雜的 A/B 測試和分析功能

技術選型

核心元件一覽

元件

技術選擇

選擇理由

前端

Next.js 14 (App Router)

全棧開發、內建 API Routes、SSR/SSG 優化、部署簡單

資料庫

Supabase (PostgreSQL)

免費額度充足、實時訂閱、Edge Functions、完整開發者工具

Email 發送

Resend

API 簡潔直觀、React Email 整合、完整 Webhook 追蹤、價格透明

部署

Vercel

與 Next.js 完美整合、零配置部署、自動化 CI/CD

為什麼選 Resend?

雖然市面上有多個 Email API 服務,但 Resend 對於開發者來說最具優勢:

服務

免費額度

核心優勢

Resend

100 封/天、3,000 封/月

API 簡潔、React Email 支援、完整 Webhook、無隱藏費用

SendGrid

100 封/天

功能完整但設定複雜

Mailgun

5,000 封/月(3個月試用)

以驗證和基礎設施為主

Amazon SES

$0.10 / 1,000 封

低廉但設定繁複、冷啟動困難

Resend 的三大優勢:

  1. API 極簡 — 一個 POST 請求即可發信,無需複雜的設定

  2. 完整 Webhook — sent、delivered、opened、clicked、bounced、complained 全追蹤

  3. React Email — 用熟悉的 React 元件寫 Email 模板,維護性高

系統架構設計

整體流程圖

┌──────────────────────────────────────────────┐
│           管理員介面 (Dashboard)              │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐   │
│  │訂閱者管理 │  │活動管理  │  │分析報表  │   │
│  └──────────┘  └──────────┘  └──────────┘   │
└──────────────────────────────────────────────┘
                     ▼
┌──────────────────────────────────────────────┐
│         Next.js 後端 API Routes              │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐   │
│  │/api/send │  │ /api/    │  │/api/     │   │
│  │          │  │ webhook  │  │subscribe │   │
│  └──────────┘  └──────────┘  └──────────┘   │
└──────────────────────────────────────────────┘
     ▼              ▼              ▼
┌──────────┐  ┌──────────┐  ┌──────────┐
│ Resend   │  │Supabase  │  │Supabase  │
│ API      │  │Database  │  │Edge Func │
│(發送郵件)│  │(資料儲存)│  │(排程任務)│
└──────────┘  └──────────┘  └──────────┘

資料庫核心設計

四張關鍵資料表

整個系統以四張精心設計的資料表為核心:

1️⃣ subscribers(訂閱者表)

CREATE TABLE subscribers (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email VARCHAR(255) NOT NULL UNIQUE,
  first_name VARCHAR(100),
  name VARCHAR(255),
  company VARCHAR(255),
  position VARCHAR(100),

  -- 分群與來源追蹤
  tier VARCHAR(50),           -- Tier1/Tier2/Tier3 等級
  source VARCHAR(50),         -- 來源:website/manual/import

  -- 狀態管理
  status VARCHAR(20) DEFAULT 'active',      -- active/unsubscribed/paused
  email_status VARCHAR(20) DEFAULT 'active', -- active/bounced/complained

  -- 時間戳
  created_at TIMESTAMPTZ DEFAULT NOW(),
  unsubscribed_at TIMESTAMPTZ,
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

設計思路: - tier 欄位支援分層策略 — 不同等級訂閱者接收不同內容或頻率 - email_status 自動追蹤 Email 健康度 — Webhook 退信自動標記為 bounced - source 記錄訂閱來源 — 便於分析各渠道轉換效率

2️⃣ edm_campaigns(發送活動表)

CREATE TABLE edm_campaigns (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  subject TEXT NOT NULL,
  preview_text TEXT,
  content TEXT NOT NULL,

  -- 目標設定
  target_tier TEXT,          -- 指定分群投放
  target_count INTEGER,      -- 實際投放人數

  -- 發送控制
  emails_per_day INTEGER DEFAULT 100,
  status TEXT DEFAULT 'draft',  -- draft/scheduled/sending/completed/cancelled
  scheduled_at TIMESTAMPTZ,

  -- 統計數據
  sent_count INTEGER DEFAULT 0,
  success_count INTEGER DEFAULT 0,
  failed_count INTEGER DEFAULT 0,
  open_count INTEGER DEFAULT 0,
  click_count INTEGER DEFAULT 0,

  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

設計思路: - emails_per_day 控制每日發送量 — 避免超過服務商配額 - status 狀態機嚴格控制發送流程 — 防止誤操作 - 即時統計欄位 — Webhook 更新時同步更新,提供實時儀表板

3️⃣ edmsendqueue(發送佇列表)

CREATE TABLE edm_send_queue (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  campaign_id UUID REFERENCES edm_campaigns(id),
  subscriber_id UUID REFERENCES subscribers(id),

  -- 收件人快照(防止後續修改影響記錄)
  email TEXT NOT NULL,
  first_name TEXT,
  name TEXT,

  -- 發送狀態
  status TEXT DEFAULT 'pending',  -- pending/sent/failed/bounced
  batch_number INTEGER,
  scheduled_date DATE,

  -- Resend 追蹤 ID
  resend_email_id TEXT UNIQUE,
  
  -- Email 生命週期追蹤
  sent_at TIMESTAMPTZ,
  delivered_at TIMESTAMPTZ,
  opened_at TIMESTAMPTZ,
  clicked_at TIMESTAMPTZ,
  bounced_at TIMESTAMPTZ,
  complained_at TIMESTAMPTZ,

  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- 創建索引提升查詢效能
CREATE INDEX idx_edm_queue_campaign ON edm_send_queue(campaign_id);
CREATE INDEX idx_edm_queue_status ON edm_send_queue(status);
CREATE INDEX idx_edm_queue_scheduled ON edm_send_queue(scheduled_date);
CREATE INDEX idx_edm_queue_resend_id ON edm_send_queue(resend_email_id);

設計思路: - 收件人快照 — 儲存發送時的收件人資訊,確保歷史記錄的準確性(即使日後修改訂閱者資訊也不影響) - Resend Email ID— 對應 Resend 返回的追蹤 ID,用於匹配 Webhook 事件 - 完整生命週期追蹤 — 從 sent 到 opened/clicked,記錄每一步時間戳

4️⃣ email_events(事件記錄表)

CREATE TABLE email_events (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  queue_id UUID REFERENCES edm_send_queue(id),
  resend_email_id TEXT NOT NULL,
  event_type TEXT NOT NULL,  -- sent/delivered/opened/clicked/bounced/complained
  event_data JSONB,          -- 完整 Webhook 原始資料
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_email_events_queue ON email_events(queue_id);
CREATE INDEX idx_email_events_type ON email_events(event_type);
CREATE INDEX idx_email_events_resend_id ON email_events(resend_email_id);

設計思路: - 完整事件記錄 — 儲存所有 Webhook 事件,便於除錯和分析異常 - JSONB 原始資料 — 保留最大彈性,日後可增加新的分析維度

資料關聯圖

┌─────────────────┐
│   subscribers   │
│   (訂閱者)       │
└────────┬────────┘
         │
         │ (多對多)
         │
         ▼
┌──────────────────────┐
│  edm_send_queue      │
│  (發送佇列)          │
└────────┬─────────────┘
         │
         │ (一對多)
         │
         ▼
┌──────────────────────┐
│   email_events       │
│   (事件記錄)         │
└──────────────────────┘

┌──────────────────────┐
│  edm_campaigns       │  (一對多)
│  (活動)              │──────→ edm_send_queue
└──────────────────────┘

智慧分批發送機制

為了避免超過 Resend 的免費配額限制(3,000 封/月),我們設計了智慧分批發送機制:

分批邏輯

假設活動目標 5,000 人,每日配額 100 封:

Day 1: 發送 Queue #1 ~ #100      (100 封)
Day 2: 發送 Queue #101 ~ #200    (100 封)
Day 3: 發送 Queue #201 ~ #300    (100 封)
...
Day 50: 發送剩餘項目             (完成)

實作步驟

步驟 1:建立佇列時分配批次號

WITH numbered_subscribers AS (
  SELECT
    sub.id,
    sub.email,
    sub.first_name,
    ROW_NUMBER() OVER (ORDER BY sub.created_at) as row_num
  FROM subscribers sub
  WHERE sub.status = 'active'
    AND sub.email_status = 'active'
    AND (target_tier IS NULL OR sub.tier = target_tier)
)
INSERT INTO edm_send_queue (
  campaign_id, subscriber_id, email, first_name,
  batch_number, scheduled_date
)
SELECT
  p_campaign_id,
  id,
  email,
  first_name,
  CEIL(row_num::NUMERIC / p_daily_limit) as batch_number,
  CURRENT_DATE + (CEIL(row_num::NUMERIC / p_daily_limit) - 1)
FROM numbered_subscribers;

步驟 2:排程任務每日執行

// Supabase Edge Function 或 Next.js 後端定時任務
export async function sendDailyBatch() {
  const today = new Date().toISOString().split('T')[0];
  
  const { data: queueItems, error } = await supabase
    .from('edm_send_queue')
    .select('*')
    .eq('status', 'pending')
    .lte('scheduled_date', today)
    .limit(100);  // 每日限制 100 封

  for (const item of queueItems) {
    await sendViaResend(item);
  }
}

配額管理機制

建立簡單但有效的配額監控:

CREATE TABLE email_quota (
  date DATE PRIMARY KEY,
  sent_count INTEGER DEFAULT 0,
  monthly_sent INTEGER DEFAULT 0
);

CREATE FUNCTION get_quota_status()
RETURNS TABLE (
  today_sent INTEGER,
  today_remaining INTEGER,
  monthly_sent INTEGER,
  monthly_remaining INTEGER,
  can_send_today BOOLEAN
) AS $$
BEGIN
  RETURN QUERY
  SELECT
    COALESCE(eq.sent_count, 0) as today_sent,
    GREATEST(0, 100 - COALESCE(eq.sent_count, 0)) as today_remaining,
    COALESCE(eq.monthly_sent, 0) as monthly_sent,
    GREATEST(0, 3000 - COALESCE(eq.monthly_sent, 0)) as monthly_remaining,
    COALESCE(eq.sent_count, 0) < 100 as can_send_today
  FROM email_quota eq
  WHERE eq.date = CURRENT_DATE;
END;
$$ LANGUAGE plpgsql;

下一篇預告

這篇介紹了完整的系統架構和資料庫設計,下一篇將深入實現細節,涵蓋:

  • Resend API 基本使用 — SDK 初始化、基本發送

  • HTML Email 模板處理 — React Email 元件編寫

  • 變數替換機制 — 個性化郵件內容的實作

  • 錯誤處理與重試策略 — 保證可靠性


系列文章導航

  1. ✅ 整體架構設計與技術選型(本篇)

  2. Resend API 串接實戰(連載中)

  3. Webhook 追蹤系統實作(規劃中)

  4. 訂閱者管理與分群策略(規劃中)

  5. 發送最佳化與異常處理(規劃中)


對這套系統有想法嗎? 歡迎在下方評論或聯繫我分享你的經驗!

Enjoyed this article? Show some love!

0
Clap

Enjoyed this article?

Subscribe for engineering notes and AI development insights

We respect your privacy. No spam, unsubscribe anytime.

Share this article

Comments