Skip to main content

Email Unsubscribe System

A secure token-based unsubscribe system that doesn't require users to enter their email address.

How It Works

1. Token Generation

When a user subscribes to your newsletter, a unique unsubscribeToken is automatically generated and stored with their subscription:

// This happens automatically in the Subscription model
model Subscription {
id String @id @default(cuid())
email String @unique
unsubscribeToken String @unique @default(cuid())
// ... other fields
}

Use the helper functions from src/utils/emailLinks.ts to generate unsubscribe links:

import { prisma } from "@/lib/prisma";
import {
generateUnsubscribeLink,
getUnsubscribeFooterHtml,
getUnsubscribeFooterText,
} from "@/utils/emailLinks";

// Get the subscription
const subscription = await prisma.subscription.findUnique({
where: { email: "user@example.com" },
});

// Generate unsubscribe link
const unsubscribeUrl = generateUnsubscribeLink(subscription.unsubscribeToken);

// Or get ready-made HTML footer
const htmlFooter = getUnsubscribeFooterHtml(subscription.unsubscribeToken);

// Or plain text footer
const textFooter = getUnsubscribeFooterText(subscription.unsubscribeToken);

3. Unsubscribe Flow

When a user clicks the unsubscribe link:

  1. Token Verification: The page loads with ?token=xxx parameter
  2. Email Masking: The backend verifies the token and returns a masked email (e.g., j******@example.com)
  3. Reason Selection: User optionally selects why they're unsubscribing
  4. Confirmation: Subscription is marked as inactive and reason is saved

API Endpoints

GET /api/unsubscribe?token=xxx

Verifies the unsubscribe token and returns masked email.

Response:

{
"maskedEmail": "j******@example.com",
"active": true
}

POST /api/unsubscribe

Unsubscribes a user using their token.

Request Body:

{
"token": "clxxx...",
"reason": "No longer interested" // Optional
}

Response:

{
"message": "You've been successfully unsubscribed",
"email": "john@example.com"
}

Unsubscribe Reasons

Users can optionally provide a reason for unsubscribing:

  • No longer interested
  • Too many emails
  • Products not relevant to me
  • I didn't sign up for this
  • Privacy concerns
  • Other

These reasons are stored in the unsubscribeReason field for analytics.

Security Features

  1. Unique Tokens: Each subscription has a unique, unguessable token
  2. No Email Input: Users don't need to enter their email (prevents typos and abuse)
  3. Email Masking: Users see a masked version of their email for confirmation
  4. Token Validation: Invalid or expired tokens are rejected
  5. Soft Delete: Subscriptions are marked inactive (not deleted) for record-keeping

Database Schema

model Subscription {
id String @id @default(cuid())
email String @unique
active Boolean @default(true)
source String @default("online-store")
reason String? // Why they subscribed
unsubscribeToken String @unique @default(cuid())
unsubscribeReason String? // Why they unsubscribed
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@index([email])
@@index([active])
@@index([unsubscribeToken])
}

Example Email Integration

Using ZeptoMail

import { sendEmail } from "@/utils/zeptomailClient";
import { generateUnsubscribeLink } from "@/utils/emailLinks";

async function sendNewsletterEmail(subscription: Subscription) {
const unsubscribeUrl = generateUnsubscribeLink(subscription.unsubscribeToken);

await sendEmail({
to: subscription.email,
subject: "New Products Launching Soon!",
htmlBody: `
<h1>Exciting News!</h1>
<p>We're launching new products next month...</p>

<hr style="margin-top: 40px; border: none; border-top: 1px solid #e5e7eb;">
<p style="font-size: 12px; color: #6b7280; text-align: center;">
<a href="${unsubscribeUrl}">Unsubscribe</a>
</p>
`,
textBody: `
Exciting News!

We're launching new products next month...

---
To unsubscribe: ${unsubscribeUrl}
`,
});
}

Best Practices

  1. Always Include Unsubscribe Links: Required by email regulations (CAN-SPAM, GDPR)
  2. Make It Easy: Users should be able to unsubscribe in 1-2 clicks
  3. Don't Ask for Email: Use token-based unsubscribe for better UX
  4. Track Reasons: Understanding why users unsubscribe helps improve your emails
  5. Honor Immediately: Mark subscriptions inactive as soon as requested
  6. Keep Records: Soft delete (mark inactive) rather than hard delete

Testing

To test the unsubscribe flow:

  1. Subscribe via the homepage form
  2. Check the database for the generated unsubscribeToken:
    SELECT email, unsubscribeToken FROM "Subscription";
  3. Visit: http://localhost:3000/unsubscribe?token=YOUR_TOKEN
  4. Select a reason and unsubscribe
  5. Verify the subscription is marked as active: false