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
}
2. Including Unsubscribe Links in Emails
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:
- Token Verification: The page loads with
?token=xxxparameter - Email Masking: The backend verifies the token and returns a masked email (e.g.,
j******@example.com) - Reason Selection: User optionally selects why they're unsubscribing
- 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
- Unique Tokens: Each subscription has a unique, unguessable token
- No Email Input: Users don't need to enter their email (prevents typos and abuse)
- Email Masking: Users see a masked version of their email for confirmation
- Token Validation: Invalid or expired tokens are rejected
- 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
- Always Include Unsubscribe Links: Required by email regulations (CAN-SPAM, GDPR)
- Make It Easy: Users should be able to unsubscribe in 1-2 clicks
- Don't Ask for Email: Use token-based unsubscribe for better UX
- Track Reasons: Understanding why users unsubscribe helps improve your emails
- Honor Immediately: Mark subscriptions inactive as soon as requested
- Keep Records: Soft delete (mark inactive) rather than hard delete
Testing
To test the unsubscribe flow:
- Subscribe via the homepage form
- Check the database for the generated
unsubscribeToken:SELECT email, unsubscribeToken FROM "Subscription"; - Visit:
http://localhost:3000/unsubscribe?token=YOUR_TOKEN - Select a reason and unsubscribe
- Verify the subscription is marked as
active: false