Skip to main content
RapidDev - Software Development Agency

How to Build Email automation with V0

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.

What you'll build

  • Subscriber management with tag filtering, bulk actions, and CSV import using shadcn/ui Table and Checkbox
  • Campaign editor with subject line, HTML body preview, and scheduling via Textarea and DatePicker
  • Drip sequence builder with step cards showing delay and content in a vertical timeline layout
  • Open tracking via 1x1 transparent pixel served from an API route with Cache-Control: no-store
  • Dashboard with open rates, click rates, and campaign performance using Recharts BarChart
  • Vercel Cron job that processes pending sequence steps every 15 minutes
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced12 min read2-4 hoursV0 Premium or higherApril 2026RapidDev Engineering Team
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.

What you're building

Email marketing remains one of the highest-ROI channels for businesses. Whether you need automated welcome sequences, product launch campaigns, or drip nurture flows, an email automation system lets you send the right message at the right time without manual effort.

V0 accelerates this by generating the subscriber management UI, campaign editor, and sequence builder from prompts. Use prompt queuing to build all three interfaces in one session. Resend handles reliable email delivery with built-in tracking, and Vercel Cron jobs automate the drip sequence processing.

The architecture uses Next.js App Router with Server Components for the dashboard and subscriber list, client components for the campaign editor, API routes for email sending and webhook processing, Supabase for all data storage, and Vercel Cron for scheduled sequence step execution.

Final result

A complete email automation platform with subscriber management, campaign sending, automated drip sequences, open tracking, and delivery analytics.

Tech stack

V0AI Code Generator
Next.jsFull-Stack Framework
Tailwind CSSStyling
shadcn/uiComponent Library
SupabaseDatabase
ResendEmail Delivery
Vercel CronScheduled Jobs

Prerequisites

  • 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)

Build steps

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.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build an email automation system. Create a Supabase schema with:
3// 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)
4// 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)
5// 3. sequences: id (uuid PK), name (text), trigger_event (text), created_by (uuid FK to auth.users), is_active (boolean default true)
6// 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)
7// 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)
8// 6. tracking_pixels: id (uuid PK), email_log_id (uuid FK), token (text unique default gen_random_uuid())
9// 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.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a subscriber management page at app/subscribers/page.tsx.
3// Requirements:
4// - Server Component that fetches subscribers from Supabase with pagination (20 per page)
5// - 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
6// - Add filter controls above the table: Select for status filter, Input for search by email, and a tag filter using Command/Popover
7// - Bulk action buttons above the table: "Delete Selected" and "Tag Selected" that appear when checkboxes are checked
8// - An "Import CSV" Button that opens a Dialog with a file upload Input accepting .csv files
9// - The import Server Action parses the CSV, validates emails with Zod, and bulk inserts into subscribers
10// - An "Add Subscriber" Button that opens a Dialog with email Input, first_name Input, and tags Input
11// - 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.

app/api/send/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { Resend } from 'resend'
3import { createClient } from '@supabase/supabase-js'
4
5const resend = new Resend(process.env.RESEND_API_KEY)
6const supabase = createClient(
7 process.env.SUPABASE_URL!,
8 process.env.SUPABASE_SERVICE_ROLE_KEY!
9)
10
11export async function POST(req: NextRequest) {
12 const { subscriberIds, subject, htmlBody, campaignId } = await req.json()
13
14 const { data: subscribers } = await supabase
15 .from('subscribers')
16 .select('id, email, first_name')
17 .in('id', subscriberIds)
18 .eq('status', 'active')
19
20 if (!subscribers?.length) {
21 return NextResponse.json({ error: 'No active subscribers' }, { status: 400 })
22 }
23
24 const results = []
25
26 for (const sub of subscribers) {
27 const { data: logEntry } = await supabase
28 .from('email_logs')
29 .insert({
30 subscriber_id: sub.id,
31 campaign_id: campaignId,
32 status: 'queued',
33 })
34 .select()
35 .single()
36
37 const { data: pixel } = await supabase
38 .from('tracking_pixels')
39 .insert({ email_log_id: logEntry!.id })
40 .select('token')
41 .single()
42
43 const personalizedHtml = htmlBody
44 .replace('{{first_name}}', sub.first_name || 'there')
45 + `<img src="${req.nextUrl.origin}/api/track/${pixel!.token}" width="1" height="1" alt="" />`
46
47 const { error } = await resend.emails.send({
48 from: 'newsletter@yourdomain.com',
49 to: sub.email,
50 subject,
51 html: personalizedHtml,
52 })
53
54 await supabase
55 .from('email_logs')
56 .update({ status: error ? 'bounced' : 'sent', sent_at: new Date().toISOString() })
57 .eq('id', logEntry!.id)
58
59 results.push({ email: sub.email, success: !error })
60 }
61
62 return NextResponse.json({ results })
63}

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.

app/api/track/[token]/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@supabase/supabase-js'
3
4const supabase = createClient(
5 process.env.SUPABASE_URL!,
6 process.env.SUPABASE_SERVICE_ROLE_KEY!
7)
8
9const TRANSPARENT_GIF = Buffer.from(
10 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
11 'base64'
12)
13
14export async function GET(
15 req: NextRequest,
16 { params }: { params: Promise<{ token: string }> }
17) {
18 const { token } = await params
19
20 const { data: pixel } = await supabase
21 .from('tracking_pixels')
22 .select('email_log_id')
23 .eq('token', token)
24 .single()
25
26 if (pixel) {
27 await supabase
28 .from('email_logs')
29 .update({ status: 'opened', opened_at: new Date().toISOString() })
30 .eq('id', pixel.email_log_id)
31 .is('opened_at', null)
32 }
33
34 return new NextResponse(TRANSPARENT_GIF, {
35 headers: {
36 'Content-Type': 'image/gif',
37 'Cache-Control': 'no-store, no-cache, must-revalidate',
38 },
39 })
40}

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.

app/api/cron/sequences/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@supabase/supabase-js'
3import { Resend } from 'resend'
4
5const supabase = createClient(
6 process.env.SUPABASE_URL!,
7 process.env.SUPABASE_SERVICE_ROLE_KEY!
8)
9const resend = new Resend(process.env.RESEND_API_KEY)
10
11export async function GET(req: NextRequest) {
12 const authHeader = req.headers.get('authorization')
13 if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
14 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
15 }
16
17 const { data: activeSequences } = await supabase
18 .from('sequences')
19 .select('id, sequence_steps(*)')
20 .eq('is_active', true)
21
22 let sent = 0
23
24 for (const seq of activeSequences ?? []) {
25 for (const step of seq.sequence_steps) {
26 const cutoff = new Date(
27 Date.now() - step.delay_hours * 60 * 60 * 1000
28 ).toISOString()
29
30 const { data: eligibleSubs } = await supabase
31 .from('subscribers')
32 .select('id, email, first_name')
33 .eq('status', 'active')
34 .lte('subscribed_at', cutoff)
35 .not('id', 'in', `(SELECT subscriber_id FROM email_logs WHERE sequence_id = '${seq.id}' AND step_order = ${step.step_order})`)
36
37 for (const sub of eligibleSubs ?? []) {
38 await resend.emails.send({
39 from: 'newsletter@yourdomain.com',
40 to: sub.email,
41 subject: step.subject.replace('{{first_name}}', sub.first_name || 'there'),
42 html: step.html_body,
43 })
44
45 await supabase.from('email_logs').insert({
46 subscriber_id: sub.id,
47 sequence_id: seq.id,
48 step_order: step.step_order,
49 status: 'sent',
50 sent_at: new Date().toISOString(),
51 })
52 sent++
53 }
54 }
55 }
56
57 return NextResponse.json({ processed: sent })
58}

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.

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

app/api/track/[token]/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@supabase/supabase-js'
3
4const supabase = createClient(
5 process.env.SUPABASE_URL!,
6 process.env.SUPABASE_SERVICE_ROLE_KEY!
7)
8
9const TRANSPARENT_GIF = Buffer.from(
10 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
11 'base64'
12)
13
14export async function GET(
15 req: NextRequest,
16 { params }: { params: Promise<{ token: string }> }
17) {
18 const { token } = await params
19
20 const { data: pixel } = await supabase
21 .from('tracking_pixels')
22 .select('email_log_id')
23 .eq('token', token)
24 .single()
25
26 if (pixel) {
27 await supabase
28 .from('email_logs')
29 .update({
30 status: 'opened',
31 opened_at: new Date().toISOString(),
32 })
33 .eq('id', pixel.email_log_id)
34 .is('opened_at', null)
35 }
36
37 return new NextResponse(TRANSPARENT_GIF, {
38 headers: {
39 'Content-Type': 'image/gif',
40 'Cache-Control': 'no-store, no-cache, must-revalidate',
41 },
42 })
43}

Customization ideas

Add click tracking with redirect links

Wrap all links in emails with a redirect route (/api/click/[token]) that logs the click event before redirecting to the original URL.

Add A/B testing for subject lines

Split subscribers into groups, send different subject lines, measure open rates after 4 hours, then send the winning version to the remaining subscribers.

Add unsubscribe handling

Include a one-click unsubscribe link in every email footer that hits an API route to update the subscriber status to 'unsubscribed' and show a confirmation page.

Add template library

Create a library of reusable email templates with variables like first_name and company that can be selected when creating new campaigns or sequence steps.

Add segment builder with conditions

Build a visual segment builder that filters subscribers by tags, signup date, engagement score, and past campaign interactions for targeted sends.

Common pitfalls

Pitfall: Not setting Cache-Control: no-store on the tracking pixel response

How to avoid: Return the tracking pixel with headers Cache-Control: no-store, no-cache, must-revalidate to ensure every open triggers a server request.

Pitfall: Forgetting to authenticate the Vercel Cron endpoint

How to avoid: Check the Authorization header against a CRON_SECRET environment variable. Vercel automatically sends this header for cron invocations.

Pitfall: Sending emails synchronously in the API route without batching

How to avoid: 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.

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building an email automation system with Next.js and Resend. I need to implement open tracking using a transparent 1x1 GIF pixel. Show me how to create the tracking pixel injection at send time, the tracking endpoint that records opens, and how to calculate open rates from the email_logs table. Include proper Cache-Control headers to prevent caching.

Build Prompt

Build the drip sequence builder UI for an email automation system. Create app/sequences/[id]/page.tsx as a 'use client' component with a vertical timeline layout. Each step is a Card showing step_order Badge, delay_hours Input, subject Input, and html_body Textarea. Add a Button to add new steps at the bottom. Include a Switch at the top to activate/deactivate the sequence. Use drag-and-drop reordering with dnd-kit. Save changes via Server Action.

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.