Email Unsubscribe Management System
Proper unsubscribe management is not only courteous but also a legal requirement (GDPR, CAN-SPAM, Russian Federal Law 152-FZ). Violations lead to domain blocking and fines. You need a reliable system: one-click unsubscribe, preference management, and respect for user choice.
Subscription and Preference Table
CREATE TABLE email_subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
subscription_type VARCHAR(100) NOT NULL,
-- 'marketing', 'digest', 'product_updates', 'security', 'transactional'
is_active BOOLEAN NOT NULL DEFAULT true,
unsubscribed_at TIMESTAMPTZ,
unsubscribe_reason TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE (user_id, subscription_type)
);
CREATE TABLE email_suppression_list (
email VARCHAR(255) PRIMARY KEY,
reason VARCHAR(100) NOT NULL, -- 'unsubscribe', 'bounce', 'complaint'
created_at TIMESTAMPTZ DEFAULT now()
);
Generating a Signed Unsubscribe Link
import { createHmac } from 'crypto';
function generateUnsubscribeToken(userId: string, type: string): string {
const payload = `${userId}:${type}:${Date.now()}`;
const signature = createHmac('sha256', process.env.UNSUBSCRIBE_SECRET!)
.update(payload)
.digest('hex');
return Buffer.from(`${payload}:${signature}`).toString('base64url');
}
function generateUnsubscribeUrl(userId: string, type: string = 'all'): string {
const token = generateUnsubscribeToken(userId, type);
return `https://app.example.com/unsubscribe/${token}`;
}
Unsubscribe Handler
// GET /unsubscribe/:token — link in email (one-click preview)
app.get('/unsubscribe/:token', async (req, res) => {
const decoded = parseUnsubscribeToken(req.params.token);
if (!decoded) return res.status(400).render('unsubscribe-invalid');
// Show confirmation page / preference management
res.render('unsubscribe', {
userId: decoded.userId,
type: decoded.type,
token: req.params.token,
});
});
// POST /unsubscribe/:token — confirm unsubscribe
app.post('/unsubscribe/:token', async (req, res) => {
const decoded = parseUnsubscribeToken(req.params.token);
if (!decoded) return res.status(400).json({ error: 'Invalid token' });
const { type, reason } = req.body;
if (type === 'all') {
// Unsubscribe from all marketing emails
await db.query(
`UPDATE email_subscriptions
SET is_active = false, unsubscribed_at = now(), unsubscribe_reason = $1
WHERE user_id = $2 AND subscription_type != 'transactional'`,
[reason, decoded.userId]
);
// Add to suppression list
const user = await db.users.findById(decoded.userId);
await db.query(
`INSERT INTO email_suppression_list (email, reason) VALUES ($1, 'unsubscribe')
ON CONFLICT (email) DO NOTHING`,
[user.email]
);
} else {
// Unsubscribe from specific type
await db.query(
`UPDATE email_subscriptions
SET is_active = false, unsubscribed_at = now()
WHERE user_id = $1 AND subscription_type = $2`,
[decoded.userId, type]
);
}
res.json({ ok: true });
});
List-Unsubscribe Header (RFC 8058)
Gmail and Outlook show an "Unsubscribe" button in the interface if the email contains the List-Unsubscribe header. This is mandatory for senders > 5,000 emails per day since February 2024:
await sendEmail({
to: user.email,
subject: 'Our digest',
html: emailHtml,
headers: {
// RFC 2369 — mailto for email clients
'List-Unsubscribe': `<mailto:[email protected]?subject=unsub-${userId}>, <https://app.example.com/unsubscribe/${token}>`,
// RFC 8058 — POST request without opening browser (Gmail one-click)
'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',
},
});
// POST /api/email/list-unsubscribe — One-Click handler
app.post('/api/email/list-unsubscribe', async (req, res) => {
const { 'list-unsubscribe' }: { 'list-unsubscribe': string } = req.body;
// Field = 'One-Click' — just process it
// User identification — via X-Token header or token in URL
res.status(200).end();
});
Preference Management Page
Instead of complete unsubscribe — offer users to choose subscription types:
| Type | Description | Disableable |
|---|---|---|
transactional |
Confirmations, invoices | No |
security |
Login from new device | No |
product_updates |
Product updates | Yes |
marketing |
Promos and discounts | Yes |
digest |
Weekly digest | Yes |
Timeline
An unsubscribe system with one-click link, List-Unsubscribe header, preference management page takes 2–3 days.







