Build a newsletter subscription system with V0 using Next.js, Supabase, Resend, and shadcn/ui. Features email capture with double opt-in confirmation, preference management, subscriber list admin, and seamless integration with Resend for transactional email delivery — all in about 30-60 minutes.
What you're building
Every startup and creator needs an email list. A double opt-in newsletter system ensures high deliverability and compliance while giving subscribers control over their preferences.
V0 makes this the perfect beginner project — describe the signup form in chat, V0 generates the full component, then use Design Mode (Option+D) to adjust colors and spacing for free. No complex integrations needed.
The architecture uses a simple API route for subscription and confirmation, Supabase for subscriber data, and Resend for transactional email delivery. The entire system can be built with the V0 free tier.
Final result
A newsletter subscription system with double opt-in email verification, preference management, admin subscriber list, and Resend email delivery.
Tech stack
Prerequisites
- A V0 account (free tier works for this project)
- A Supabase project (free tier works — connect via V0's Connect panel)
- A Resend account (free tier: 100 emails/day)
- A verified domain or email address in Resend
Build steps
Set up the database schema for subscribers and lists
Create the Supabase schema for subscribers, newsletter lists, and list assignments. The subscribers table includes a confirmation token for double opt-in verification.
1// Paste this prompt into V0's AI chat:2// Build a newsletter subscription system. Create a Supabase schema:3// 1. subscribers: id (uuid PK), email (text UNIQUE), name (text), status (text DEFAULT 'pending' CHECK IN 'pending','active','unsubscribed'), confirmation_token (uuid DEFAULT gen_random_uuid()), preferences (jsonb DEFAULT '{}'), subscribed_at (timestamptz DEFAULT now()), confirmed_at (timestamptz)4// 2. lists: id (uuid PK), name (text), slug (text UNIQUE), description (text)5// 3. subscriber_lists: subscriber_id (uuid FK to subscribers), list_id (uuid FK to lists), PRIMARY KEY (subscriber_id, list_id)6// Seed lists: 'Weekly Digest', 'Product Updates', 'Tips & Tutorials'.7// Add RLS policies. Generate SQL and TypeScript types.Pro tip: V0's beginner-friendly workflow makes this perfect as a first project. Describe the signup form in chat, V0 generates it, then use Design Mode (Option+D) to tweak colors for free.
Expected result: Supabase is connected with subscribers, lists, and subscriber_lists tables. Three newsletter lists are seeded.
Build the signup form and subscribe API
Create the landing page with an email signup widget and the API route that creates the subscriber with a pending status, generates a confirmation token, and sends the confirmation email via Resend.
1import { createClient } from '@supabase/supabase-js'2import { NextRequest, NextResponse } from 'next/server'3import { Resend } from 'resend'45const supabase = createClient(6 process.env.SUPABASE_URL!,7 process.env.SUPABASE_SERVICE_ROLE_KEY!8)9const resend = new Resend(process.env.RESEND_API_KEY)1011export async function POST(req: NextRequest) {12 const { email, name } = await req.json()1314 if (!email || !email.includes('@')) {15 return NextResponse.json({ error: 'Invalid email' }, { status: 400 })16 }1718 const { data: existing } = await supabase19 .from('subscribers')20 .select('id, status')21 .eq('email', email)22 .single()2324 if (existing?.status === 'active') {25 return NextResponse.json({ error: 'Already subscribed' }, { status: 409 })26 }2728 const { data: subscriber, error } = await supabase29 .from('subscribers')30 .upsert({ email, name, status: 'pending' })31 .select('id, confirmation_token')32 .single()3334 if (error) {35 return NextResponse.json({ error: error.message }, { status: 500 })36 }3738 const confirmUrl = `${process.env.NEXT_PUBLIC_APP_URL}/confirm?token=${subscriber.confirmation_token}`3940 await resend.emails.send({41 from: 'newsletter@yourdomain.com',42 to: email,43 subject: 'Confirm your subscription',44 html: `<p>Hi ${name || 'there'},</p><p>Click <a href="${confirmUrl}">here</a> to confirm your subscription.</p>`,45 })4647 return NextResponse.json({ success: true })48}Expected result: Submitting the form creates a pending subscriber and sends a confirmation email with a unique token link.
Build the confirmation page and preferences manager
Create the token verification page that activates the subscription and a preferences page where subscribers manage their newsletter list selections.
1// Paste this prompt into V0's AI chat:2// Create subscription pages:3// 1. app/confirm/page.tsx — reads 'token' from URL searchParams, calls /api/confirm to validate and activate. Shows success Card with checkmark or error Card with retry link.4// 2. app/preferences/page.tsx — reads 'token' from URL. Shows the subscriber's current list selections as Checkbox group. Save Button updates subscriber_lists via Server Action. Add an 'Unsubscribe from all' Button with AlertDialog confirmation.5// 3. app/api/confirm/route.ts — GET that validates the token, updates subscriber status to 'active', sets confirmed_at, nullifies the token.6// Use shadcn/ui Card for the confirmation message, Checkbox for list selection, Toast for success feedback.Expected result: Clicking the email link activates the subscription. The preferences page lets subscribers choose which lists they receive.
Build the admin subscriber dashboard and deploy
Create the admin page showing all subscribers with status filtering, export capability, and list management. Then deploy.
1// Paste this prompt into V0's AI chat:2// Create an admin dashboard at app/admin/subscribers/page.tsx.3// Requirements:4// - Fetch all subscribers with their list assignments5// - Display in a shadcn/ui Table: email, name, status Badge (pending=yellow, active=green, unsubscribed=red), lists joined as Badge group, subscribed_at, confirmed_at6// - Add filters: status Select, list Select, search Input7// - Add 'Export CSV' Button that downloads all subscribers as a CSV file8// - Show summary Cards: Total Subscribers, Active, Pending, Unsubscribed9// - Add bulk actions: select rows with Checkbox, bulk delete or change status10// - Protect this page with authentication checkPro tip: Set RESEND_API_KEY in V0's Vars tab without NEXT_PUBLIC_ prefix since email sending is server-only. No Stripe needed for this beginner project.
Expected result: The admin dashboard shows all subscribers with filters, status badges, and CSV export. The app is deployed via Share > Publish.
Complete code
1import { createClient } from '@supabase/supabase-js'2import { NextRequest, NextResponse } from 'next/server'3import { Resend } from 'resend'45const supabase = createClient(6 process.env.SUPABASE_URL!,7 process.env.SUPABASE_SERVICE_ROLE_KEY!8)9const resend = new Resend(process.env.RESEND_API_KEY)1011export async function POST(req: NextRequest) {12 const { email, name } = await req.json()1314 if (!email?.includes('@')) {15 return NextResponse.json({ error: 'Invalid email' }, { status: 400 })16 }1718 const { data: existing } = await supabase19 .from('subscribers')20 .select('status')21 .eq('email', email)22 .single()2324 if (existing?.status === 'active') {25 return NextResponse.json({ message: 'Already subscribed' })26 }2728 const { data, error } = await supabase29 .from('subscribers')30 .upsert({ email, name, status: 'pending' })31 .select('confirmation_token')32 .single()3334 if (error) {35 return NextResponse.json({ error: error.message }, { status: 500 })36 }3738 const url = `${process.env.NEXT_PUBLIC_APP_URL}/confirm?token=${data.confirmation_token}`3940 await resend.emails.send({41 from: 'newsletter@yourdomain.com',42 to: email,43 subject: 'Confirm your subscription',44 html: `<p>Hi ${name || 'there'},</p>45 <p><a href="${url}">Click here</a> to confirm.</p>`,46 })4748 return NextResponse.json({ success: true })49}Customization ideas
Welcome email series
After confirmation, trigger a sequence of welcome emails over the first week using Vercel Cron Jobs and the Resend API.
Referral tracking
Generate unique referral links per subscriber and track how many signups each person refers for a referral rewards program.
Embed widget for external sites
Create a lightweight JavaScript embed that other websites can include to add your newsletter signup form to their pages.
Analytics dashboard
Track subscription rates, confirmation rates, and unsubscribe rates over time with Recharts line charts on the admin page.
Common pitfalls
Pitfall: Skipping double opt-in and activating subscribers immediately
How to avoid: Always implement double opt-in: insert with status 'pending', send confirmation email with a unique token, and only set status to 'active' when the token is verified.
Pitfall: Storing RESEND_API_KEY with NEXT_PUBLIC_ prefix
How to avoid: Store RESEND_API_KEY in V0's Vars tab without any prefix. Only use it in API routes (server-side).
Pitfall: Not nullifying the confirmation token after use
How to avoid: Set confirmation_token to NULL after successful confirmation. If someone visits the URL again, show a 'already confirmed' message.
Best practices
- Always implement double opt-in for newsletter signups to ensure high deliverability and compliance
- Store RESEND_API_KEY in V0's Vars tab without NEXT_PUBLIC_ prefix — email sending is server-only
- Use V0's Design Mode (Option+D) to adjust signup form colors, Button styling, and Card layout without spending credits
- Nullify the confirmation token after use to prevent replay attacks
- Add a unique constraint on the email column to prevent duplicate subscribers
- Include an unsubscribe link in every email that links to the preferences page with the subscriber's token
AI prompts to try
Copy these prompts to build this project faster.
I'm building a newsletter subscription system with Next.js App Router, Supabase, and Resend. I need a double opt-in flow. The subscribe API should insert a subscriber with status 'pending' and a confirmation_token (UUID), then send a confirmation email via Resend with a link containing the token. The confirm API should validate the token, set status to 'active', and nullify the token. Please write both API routes.
Create a beautiful email signup widget component. It should have an email Input with placeholder 'Enter your email', an optional name Input, and a 'Subscribe' Button. Show a loading spinner in the Button while submitting. On success, replace the form with a success message: 'Check your inbox to confirm.' On error, show a red Toast with the error message. Make it reusable as a component that can be placed anywhere on the site.
Frequently asked questions
What is double opt-in and why is it important?
Double opt-in means subscribers must confirm their email by clicking a link after signing up. This prevents spam signups, ensures valid email addresses, and improves deliverability. Without it, bots can flood your list with fake emails.
Can I use the free V0 plan for this project?
Yes. The newsletter system is simple enough to build with the free tier's credits. It requires just a few prompts for the signup form, confirmation page, and admin dashboard.
How much does Resend cost?
Resend's free tier allows 100 emails per day and 3,000 per month, which is plenty for a starting newsletter. Paid plans start at $20/month for higher volume.
Can subscribers manage their own preferences?
Yes. The preferences page loads with the subscriber's token from the URL and shows checkboxes for each newsletter list. Changes are saved via a Server Action. Include this URL in every email footer.
How do I export my subscriber list?
The admin dashboard has an Export CSV button that downloads all subscribers with their email, name, status, lists, and dates as a CSV file that can be imported into any email marketing tool.
How do I deploy the newsletter system?
Click Share in V0, then Publish to Production. Set RESEND_API_KEY in the Vars tab without NEXT_PUBLIC_ prefix. Set NEXT_PUBLIC_APP_URL to your production URL for correct confirmation links.
Can RapidDev help build a custom newsletter system?
Yes. RapidDev has built over 600 apps including email marketing platforms with automation sequences, A/B testing, and analytics. Book a free consultation to discuss your email strategy.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation