# How to Build Email automation with V0

- Tool: v0
- Difficulty: Advanced
- Compatibility: V0 Premium or higher
- Last updated: April 2026

## TL;DR

Build an email automation system with V0 using Next.js, Resend for email delivery, Supabase for subscriber and campaign management, and Vercel Cron for drip sequences. You'll create a subscriber list, campaign editor, visual sequence builder, and open-tracking analytics — all in about 2-4 hours without touching a terminal.

## Before you start

- A V0 account (Premium plan recommended for the complex multi-page build)
- A Supabase project (free tier works — connect via V0's Connect panel)
- A Resend account with API key (free tier: 3,000 emails/month)
- A verified sending domain in Resend (or use the onboarding domain for testing)

## Step-by-step guide

### 1. Set up the database schema for email automation

Open V0, create a new project, and connect Supabase via the Connect panel. Then prompt V0 to create the schema for subscribers, campaigns, sequences, email logs, and tracking.

```
// Paste this prompt into V0's AI chat:
// Build an email automation system. Create a Supabase schema with:
// 1. subscribers: id (uuid PK), email (text unique), first_name (text), tags (text[]), status (text default 'active' check in 'active','unsubscribed','bounced'), subscribed_at (timestamptz default now()), unsubscribed_at (timestamptz)
// 2. campaigns: id (uuid PK), name (text), subject (text), html_body (text), plain_body (text), status (text default 'draft'), scheduled_at (timestamptz), sent_at (timestamptz), created_by (uuid FK to auth.users), created_at (timestamptz)
// 3. sequences: id (uuid PK), name (text), trigger_event (text), created_by (uuid FK to auth.users), is_active (boolean default true)
// 4. sequence_steps: id (uuid PK), sequence_id (uuid FK to sequences), step_order (int), delay_hours (int), subject (text), html_body (text), created_at (timestamptz)
// 5. email_logs: id (uuid PK), subscriber_id (uuid FK), campaign_id (uuid FK nullable), sequence_id (uuid FK nullable), step_order (int), status (text check in 'queued','sent','delivered','opened','clicked','bounced'), sent_at (timestamptz), opened_at (timestamptz)
// 6. tracking_pixels: id (uuid PK), email_log_id (uuid FK), token (text unique default gen_random_uuid())
// Add RLS policies for authenticated access.
```

> Pro tip: Queue this schema prompt first, then immediately queue the subscriber management UI prompt. V0 processes up to 10 prompts sequentially.

**Expected result:** Supabase is connected with all six tables created and RLS policies configured.

### 2. Build the subscriber management page

Create the subscriber list page with filtering by tags and status, bulk selection, and a CSV import feature. This page uses a Server Component for the initial data load and a client component for interactive filtering.

```
// Paste this prompt into V0's AI chat:
// Build a subscriber management page at app/subscribers/page.tsx.
// Requirements:
// - Server Component that fetches subscribers from Supabase with pagination (20 per page)
// - Display in a shadcn/ui Table with columns: Checkbox for bulk select, email, first_name, tags (as Badge components), status (Badge with color), subscribed_at date
// - Add filter controls above the table: Select for status filter, Input for search by email, and a tag filter using Command/Popover
// - Bulk action buttons above the table: "Delete Selected" and "Tag Selected" that appear when checkboxes are checked
// - An "Import CSV" Button that opens a Dialog with a file upload Input accepting .csv files
// - The import Server Action parses the CSV, validates emails with Zod, and bulk inserts into subscribers
// - An "Add Subscriber" Button that opens a Dialog with email Input, first_name Input, and tags Input
// - Show total subscriber count and active/unsubscribed breakdown as Card stats at the top
```

**Expected result:** The subscriber page shows a filterable, searchable table with bulk selection. CSV import and manual add dialogs work for adding new subscribers.

### 3. Create the email sending API route with open tracking

Build the API route that sends emails via Resend with an injected tracking pixel. Also create the tracking pixel route that records opens and returns a transparent 1x1 GIF.

```
import { NextRequest, NextResponse } from 'next/server'
import { Resend } from 'resend'
import { createClient } from '@supabase/supabase-js'

const resend = new Resend(process.env.RESEND_API_KEY)
const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

export async function POST(req: NextRequest) {
  const { subscriberIds, subject, htmlBody, campaignId } = await req.json()

  const { data: subscribers } = await supabase
    .from('subscribers')
    .select('id, email, first_name')
    .in('id', subscriberIds)
    .eq('status', 'active')

  if (!subscribers?.length) {
    return NextResponse.json({ error: 'No active subscribers' }, { status: 400 })
  }

  const results = []

  for (const sub of subscribers) {
    const { data: logEntry } = await supabase
      .from('email_logs')
      .insert({
        subscriber_id: sub.id,
        campaign_id: campaignId,
        status: 'queued',
      })
      .select()
      .single()

    const { data: pixel } = await supabase
      .from('tracking_pixels')
      .insert({ email_log_id: logEntry!.id })
      .select('token')
      .single()

    const personalizedHtml = htmlBody
      .replace('{{first_name}}', sub.first_name || 'there')
      + `<img src="${req.nextUrl.origin}/api/track/${pixel!.token}" width="1" height="1" alt="" />`

    const { error } = await resend.emails.send({
      from: 'newsletter@yourdomain.com',
      to: sub.email,
      subject,
      html: personalizedHtml,
    })

    await supabase
      .from('email_logs')
      .update({ status: error ? 'bounced' : 'sent', sent_at: new Date().toISOString() })
      .eq('id', logEntry!.id)

    results.push({ email: sub.email, success: !error })
  }

  return NextResponse.json({ results })
}
```

> Pro tip: For large subscriber lists, consider batching sends in groups of 50 with a small delay between batches to respect Resend's rate limits on the free tier.

**Expected result:** The send route delivers emails via Resend with tracking pixels injected. Each email is logged with its delivery status.

### 4. Build the tracking pixel endpoint

Create a GET route that serves a transparent 1x1 pixel. When an email client loads this image, it records the open event in the database. Cache-Control: no-store ensures the pixel is fetched on every view.

```
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

const TRANSPARENT_GIF = Buffer.from(
  'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
  'base64'
)

export async function GET(
  req: NextRequest,
  { params }: { params: Promise<{ token: string }> }
) {
  const { token } = await params

  const { data: pixel } = await supabase
    .from('tracking_pixels')
    .select('email_log_id')
    .eq('token', token)
    .single()

  if (pixel) {
    await supabase
      .from('email_logs')
      .update({ status: 'opened', opened_at: new Date().toISOString() })
      .eq('id', pixel.email_log_id)
      .is('opened_at', null)
  }

  return new NextResponse(TRANSPARENT_GIF, {
    headers: {
      'Content-Type': 'image/gif',
      'Cache-Control': 'no-store, no-cache, must-revalidate',
    },
  })
}
```

**Expected result:** When an email is opened, the tracking pixel loads and records the open timestamp. The route returns a transparent 1x1 GIF with no-cache headers.

### 5. Create the drip sequence processor with Vercel Cron

Build a cron endpoint that runs every 15 minutes, checks which sequence steps are due based on subscriber enrollment time and step delay, and sends the emails. Configure it in vercel.json.

```
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'
import { Resend } from 'resend'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)
const resend = new Resend(process.env.RESEND_API_KEY)

export async function GET(req: NextRequest) {
  const authHeader = req.headers.get('authorization')
  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const { data: activeSequences } = await supabase
    .from('sequences')
    .select('id, sequence_steps(*)')
    .eq('is_active', true)

  let sent = 0

  for (const seq of activeSequences ?? []) {
    for (const step of seq.sequence_steps) {
      const cutoff = new Date(
        Date.now() - step.delay_hours * 60 * 60 * 1000
      ).toISOString()

      const { data: eligibleSubs } = await supabase
        .from('subscribers')
        .select('id, email, first_name')
        .eq('status', 'active')
        .lte('subscribed_at', cutoff)
        .not('id', 'in', `(SELECT subscriber_id FROM email_logs WHERE sequence_id = '${seq.id}' AND step_order = ${step.step_order})`)

      for (const sub of eligibleSubs ?? []) {
        await resend.emails.send({
          from: 'newsletter@yourdomain.com',
          to: sub.email,
          subject: step.subject.replace('{{first_name}}', sub.first_name || 'there'),
          html: step.html_body,
        })

        await supabase.from('email_logs').insert({
          subscriber_id: sub.id,
          sequence_id: seq.id,
          step_order: step.step_order,
          status: 'sent',
          sent_at: new Date().toISOString(),
        })
        sent++
      }
    }
  }

  return NextResponse.json({ processed: sent })
}
```

**Expected result:** Every 15 minutes, the cron job checks for subscribers who are due for the next step in their sequence and sends the email automatically.

### 6. Build the campaign editor and analytics dashboard

Create the campaign editor with subject line, HTML body preview, and scheduling. Build the dashboard with open rate, click rate, and campaign comparison charts.

```
// Paste this prompt into V0's AI chat:
// Build two pages for the email automation system:
// 1. app/campaigns/new/page.tsx — a 'use client' campaign editor with:
//    - Input for campaign name and subject line
//    - Textarea for HTML email body with placeholder text
//    - A preview panel (side by side) that renders the HTML in an iframe
//    - DatePicker for scheduling send time (optional, immediate if not set)
//    - Tabs to switch between editing subscribers (select by tags using Checkbox list) and the email content
//    - "Send Now" Button and "Schedule" Button that call Server Actions
//    - Card wrapper for each section with clear labels
// 2. app/page.tsx — dashboard with:
//    - Card stats at top: total subscribers, emails sent today, average open rate, average click rate
//    - Recharts BarChart showing emails sent per day for the last 30 days
//    - Table of recent campaigns with columns: name, subject, sent_at, total sent, open rate %, status Badge
//    - Tabs for switching between Campaigns, Sequences, and Subscribers quick views
// Use shadcn/ui components throughout.
```

> Pro tip: Configure the Vercel Cron job in vercel.json: {"crons": [{"path": "/api/cron/sequences", "schedule": "*/15 * * * *"}]}. Set CRON_SECRET in the Vars tab to authenticate the endpoint.

**Expected result:** The campaign editor lets you compose emails with a live preview and schedule sends. The dashboard shows delivery stats and open rates in charts.

## Complete code example

File: `app/api/track/[token]/route.ts`

```typescript
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

const TRANSPARENT_GIF = Buffer.from(
  'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
  'base64'
)

export async function GET(
  req: NextRequest,
  { params }: { params: Promise<{ token: string }> }
) {
  const { token } = await params

  const { data: pixel } = await supabase
    .from('tracking_pixels')
    .select('email_log_id')
    .eq('token', token)
    .single()

  if (pixel) {
    await supabase
      .from('email_logs')
      .update({
        status: 'opened',
        opened_at: new Date().toISOString(),
      })
      .eq('id', pixel.email_log_id)
      .is('opened_at', null)
  }

  return new NextResponse(TRANSPARENT_GIF, {
    headers: {
      'Content-Type': 'image/gif',
      'Cache-Control': 'no-store, no-cache, must-revalidate',
    },
  })
}
```

## Common mistakes

- **Not setting Cache-Control: no-store on the tracking pixel response** — Email clients and proxies may cache the image, meaning subsequent opens are not tracked because the cached version is served instead of hitting the server. Fix: Return the tracking pixel with headers Cache-Control: no-store, no-cache, must-revalidate to ensure every open triggers a server request.
- **Forgetting to authenticate the Vercel Cron endpoint** — Without authentication, anyone can trigger the cron URL manually and cause duplicate email sends or abuse your Resend quota. Fix: Check the Authorization header against a CRON_SECRET environment variable. Vercel automatically sends this header for cron invocations.
- **Sending emails synchronously in the API route without batching** — Sending 1,000+ emails in a single request will timeout the serverless function (default 10s limit) and potentially hit Resend rate limits. Fix: Batch sends into groups of 50 with the Resend batch API, or use a background processing pattern with a queue table that the cron job drains.

## Best practices

- Inject tracking pixels at send time, not in the template, so each email gets a unique token for accurate per-recipient open tracking.
- Use Vercel Cron jobs for drip sequence processing — configure in vercel.json with CRON_SECRET authentication in the Vars tab.
- Batch email sends to respect Resend rate limits (free tier: 3,000/month, 100/day). Use the Resend batch endpoint for campaigns with many recipients.
- Store email content as both HTML and plain text for maximum deliverability across email clients.
- Use Server Components for the dashboard and subscriber list. Only add 'use client' for the campaign editor where interactive features are needed.
- Leverage V0's prompt queuing — queue the subscriber UI, campaign editor, and sequence builder as three separate prompts in one session.
- Set RESEND_API_KEY and CRON_SECRET in the Vars tab without NEXT_PUBLIC_ prefix — both are server-side only.

## Frequently asked questions

### How does open tracking work without JavaScript?

A unique 1x1 transparent GIF is injected into each email's HTML body at send time. When the recipient opens the email, their email client loads the image from your API route, which records the open event and returns the GIF. Cache-Control: no-store ensures each open is tracked.

### What email service does this use?

Resend, which offers 3,000 free emails per month and has a simple API. Set your RESEND_API_KEY in V0's Vars tab. You can swap Resend for SendGrid, Mailgun, or Amazon SES by changing the send function.

### How do drip sequences automatically send emails?

A Vercel Cron job runs every 15 minutes, checking which subscribers are due for their next sequence step based on their enrollment time and the step's delay_hours. It sends pending emails and logs the results.

### Can I schedule campaigns for a specific date and time?

Yes. The campaign editor includes a DatePicker for scheduling. Scheduled campaigns are stored with a scheduled_at timestamp. The cron job checks for campaigns where scheduled_at has passed and status is 'scheduled', then sends them.

### How do I handle unsubscribes to comply with email regulations?

Include a one-click unsubscribe link in every email footer. Create an API route at /api/unsubscribe/[token] that updates the subscriber status to 'unsubscribed' and sets unsubscribed_at. Filter out unsubscribed users in all send queries.

### Can RapidDev help build a custom email automation system?

Yes. RapidDev has built 600+ apps including complex marketing automation platforms with advanced segmentation, A/B testing, and CRM integrations. Book a free consultation to discuss your requirements.

### What V0 plan do I need for this project?

The Premium plan ($20/month) is recommended since the multi-page system requires numerous prompt iterations. The free tier can build the basic subscriber list but may require manual coding for the sequence processor and tracking system.

---

Source: https://www.rapidevelopers.com/how-to-build-v0/email-automation
© RapidDev — https://www.rapidevelopers.com/how-to-build-v0/email-automation
