# How to Build Newsletter subscriptions with V0

- Tool: v0
- Difficulty: Beginner
- Compatibility: V0 Free
- Last updated: April 2026

## TL;DR

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.

## Before you start

- 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

## Step-by-step guide

### 1. 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.

```
// Paste this prompt into V0's AI chat:
// Build a newsletter subscription system. Create a Supabase schema:
// 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)
// 2. lists: id (uuid PK), name (text), slug (text UNIQUE), description (text)
// 3. subscriber_lists: subscriber_id (uuid FK to subscribers), list_id (uuid FK to lists), PRIMARY KEY (subscriber_id, list_id)
// Seed lists: 'Weekly Digest', 'Product Updates', 'Tips & Tutorials'.
// 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.

### 2. 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.

```
import { createClient } from '@supabase/supabase-js'
import { NextRequest, NextResponse } from 'next/server'
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 POST(req: NextRequest) {
  const { email, name } = await req.json()

  if (!email || !email.includes('@')) {
    return NextResponse.json({ error: 'Invalid email' }, { status: 400 })
  }

  const { data: existing } = await supabase
    .from('subscribers')
    .select('id, status')
    .eq('email', email)
    .single()

  if (existing?.status === 'active') {
    return NextResponse.json({ error: 'Already subscribed' }, { status: 409 })
  }

  const { data: subscriber, error } = await supabase
    .from('subscribers')
    .upsert({ email, name, status: 'pending' })
    .select('id, confirmation_token')
    .single()

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }

  const confirmUrl = `${process.env.NEXT_PUBLIC_APP_URL}/confirm?token=${subscriber.confirmation_token}`

  await resend.emails.send({
    from: 'newsletter@yourdomain.com',
    to: email,
    subject: 'Confirm your subscription',
    html: `<p>Hi ${name || 'there'},</p><p>Click <a href="${confirmUrl}">here</a> to confirm your subscription.</p>`,
  })

  return NextResponse.json({ success: true })
}
```

**Expected result:** Submitting the form creates a pending subscriber and sends a confirmation email with a unique token link.

### 3. 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.

```
// Paste this prompt into V0's AI chat:
// Create subscription pages:
// 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.
// 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.
// 3. app/api/confirm/route.ts — GET that validates the token, updates subscriber status to 'active', sets confirmed_at, nullifies the token.
// 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.

### 4. Build the admin subscriber dashboard and deploy

Create the admin page showing all subscribers with status filtering, export capability, and list management. Then deploy.

```
// Paste this prompt into V0's AI chat:
// Create an admin dashboard at app/admin/subscribers/page.tsx.
// Requirements:
// - Fetch all subscribers with their list assignments
// - Display in a shadcn/ui Table: email, name, status Badge (pending=yellow, active=green, unsubscribed=red), lists joined as Badge group, subscribed_at, confirmed_at
// - Add filters: status Select, list Select, search Input
// - Add 'Export CSV' Button that downloads all subscribers as a CSV file
// - Show summary Cards: Total Subscribers, Active, Pending, Unsubscribed
// - Add bulk actions: select rows with Checkbox, bulk delete or change status
// - Protect this page with authentication check
```

> Pro 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 example

File: `app/api/subscribe/route.ts`

```typescript
import { createClient } from '@supabase/supabase-js'
import { NextRequest, NextResponse } from 'next/server'
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 POST(req: NextRequest) {
  const { email, name } = await req.json()

  if (!email?.includes('@')) {
    return NextResponse.json({ error: 'Invalid email' }, { status: 400 })
  }

  const { data: existing } = await supabase
    .from('subscribers')
    .select('status')
    .eq('email', email)
    .single()

  if (existing?.status === 'active') {
    return NextResponse.json({ message: 'Already subscribed' })
  }

  const { data, error } = await supabase
    .from('subscribers')
    .upsert({ email, name, status: 'pending' })
    .select('confirmation_token')
    .single()

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }

  const url = `${process.env.NEXT_PUBLIC_APP_URL}/confirm?token=${data.confirmation_token}`

  await resend.emails.send({
    from: 'newsletter@yourdomain.com',
    to: email,
    subject: 'Confirm your subscription',
    html: `<p>Hi ${name || 'there'},</p>
           <p><a href="${url}">Click here</a> to confirm.</p>`,
  })

  return NextResponse.json({ success: true })
}
```

## Common mistakes

- **Skipping double opt-in and activating subscribers immediately** — Single opt-in leads to spam signups, bounces, and deliverability issues. Email providers may block your domain. Fix: 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.
- **Storing RESEND_API_KEY with NEXT_PUBLIC_ prefix** — The API key allows sending emails as your domain. Exposing it means anyone can send emails from your address. Fix: Store RESEND_API_KEY in V0's Vars tab without any prefix. Only use it in API routes (server-side).
- **Not nullifying the confirmation token after use** — Reusable tokens could be shared and used to re-activate unsubscribed accounts without the subscriber's consent. Fix: 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

## 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.

---

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