| name | email-notifications |
| description | Email and notification architecture: transactional email with Resend/SendGrid, React Email templates, notification preferences (channel, frequency, opt-out), delivery tracking, in-app notifications, and push notifications. Covers the full notification stack. |
Email & Notifications Skill
When to Activate
- Sending transactional emails (welcome, reset password, receipts)
- Building notification preferences and opt-out flows
- Adding in-app notifications or activity feeds
- Setting up push notifications for web or mobile
- Email deliverability issues (landing in spam)
- Choosing between Resend, SendGrid, and Postmark for a new project
- Designing a queue-backed email pipeline that retries on failure
- Implementing GDPR-compliant unsubscribe flows with List-Unsubscribe headers
Technology Selection
| Layer | Recommended | Alternative |
|---|
| Transactional email API | Resend | SendGrid, Postmark |
| Email templates | React Email | MJML, Handlebars |
| In-app notifications | DB-backed (custom) | Novu, Courier |
| Push (web) | Web Push API | OneSignal |
| Push (mobile) | FCM / APNs | Expo Notifications |
| Notification orchestration | Novu | Courier, MagicBell |
Pattern 1: Transactional Email with Resend + React Email
import {
Html, Head, Body, Container, Heading, Text, Button, Hr, Img,
} from '@react-email/components';
interface WelcomeEmailProps {
userName: string;
loginUrl: string;
}
export function WelcomeEmail({ userName, loginUrl }: WelcomeEmailProps) {
return (
<Html>
<Head />
<Body style={{ fontFamily: 'sans-serif', backgroundColor: '#f9fafb' }}>
<Container style={{ maxWidth: '560px', margin: '0 auto', padding: '24px' }}>
<Img src="https://yourdomain.com/logo.png" width={120} alt="Logo" />
<Heading>Welcome, {userName}!</Heading>
<Text>
Your account is ready. Click below to get started.
</Text>
<Button
href={loginUrl}
style={{
backgroundColor: '#3b82f6',
color: '#fff',
padding: '12px 24px',
borderRadius: '6px',
textDecoration: 'none',
}}
>
Get started
</>
You're receiving this because you created an account.
{/* Always include unsubscribe link, even for transactional */}
);
}
import { Resend } from 'resend';
import { render } from '@react-email/render';
import { WelcomeEmail } from '../emails/WelcomeEmail';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function sendWelcomeEmail(user: { email: string; name: string }) {
const html = await render(
WelcomeEmail({
userName: user.name,
loginUrl: `${process.env.APP_URL}/login`,
})
);
const { data, error } = await resend.emails.send({
from: 'Acme <noreply@acme.com>',
to: user.email,
subject: `Welcome to Acme, ${user.name}!`,
html,
headers: { 'X-Entity-Ref-ID': `welcome-${user.id}` },
: [{ : , : }],
});
(error) {
();
}
db.(emailLogs).({
: user.,
: ,
: data!.,
: (),
});
}
Pattern 2: Notification Preferences
type NotificationType =
| 'order.shipped'
| 'order.delivered'
| 'comment.reply'
| 'mention'
| 'weekly.digest';
type NotificationChannel = 'email' | 'push' | 'in_app';
const DEFAULT_PREFERENCES: Record<NotificationType, Record<NotificationChannel, boolean>> = {
'order.shipped': { email: true, push: true, in_app: true },
'order.delivered': { email: true, push: true, in_app: true },
'comment.reply': { email: true, push: true, in_app: true },
'mention': { email: true, push: true, in_app: true },
'weekly.digest': { : , : , : },
};
(): <> {
pref = db...({
: (
(notificationPreferences., userId),
(notificationPreferences., ),
(notificationPreferences., channel)
),
});
pref?. ?? [][channel];
}
() {
[emailOk, pushOk, inAppOk] = .([
(userId, , ),
(userId, , ),
(userId, , ),
]);
.([
emailOk && (userId, , data),
pushOk && (userId, , data),
inAppOk && (userId, , data),
]);
}
Pattern 3: In-App Notifications
async function createInAppNotification(
userId: string,
type: string,
data: { title: string; body: string; link?: string; metadata?: unknown }
) {
const notification = await db.insert(notifications).values({
userId,
type,
title: data.title,
body: data.body,
link: data.link,
metadata: data.metadata,
}).returning();
notifyUser(userId, 'notification:new', notification[0]);
return notification[0];
}
async function markAsRead(userId: string, notificationId: string) {
await db
.update(notifications)
.set({ readAt: new Date() })
.where(
(
(notifications., notificationId),
(notifications., userId),
(notifications.)
)
);
}
app.(, authenticate, (req, res) => {
count = db.$count(
notifications,
(
(notifications., req..),
(notifications.)
)
);
res.({ count });
});
Pattern 4: Email Queuing (never send synchronously in request)
app.post('/auth/register', async (req, res) => {
const user = await createUser(req.body);
await sendWelcomeEmail(user);
res.json({ user });
});
app.post('/auth/register', async (req, res) => {
const user = await createUser(req.body);
await emailQueue.add('welcome', { userId: user.id });
res.json({ user });
});
emailQueue.process('welcome', async (job) => {
const user = await db.query.users.findFirst({ where: eq(users.id, job.data.userId) });
await sendWelcomeEmail(user!);
});
Email Deliverability Essentials
TXT @ "v=spf1 include:_spf.resend.com ~all"
TXT resend._domainkey "v=DKIM1; k=rsa; p=..."
TXT _dmarc "v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain.com"
const headers = {
'List-Unsubscribe': `<mailto:unsubscribe@yourdomain.com?subject=unsubscribe-${token}>`,
'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',
};
Notification Preference UI
function NotificationPreferences() {
const { data: prefs } = useQuery({ queryKey: ['notification-prefs'], queryFn: fetchPrefs });
const { mutate: updatePref } = useMutation({ mutationFn: updateNotificationPref });
const rows: { type: NotificationType; label: string }[] = [
{ type: 'order.shipped', label: 'Order shipped' },
{ type: 'comment.reply', label: 'Replies to my comments' },
{ type: 'mention', label: 'Mentions' },
{ type: 'weekly.digest', label: 'Weekly digest' },
];
return (
<table>
<thead>
<tr>
<th>Notification</th>
<th>Email</th>
<th>Push</>
In-app
{rows.map(row => (
{row.label}
{(['email', 'push', 'in_app'] as NotificationChannel[]).map(channel => (
updatePref({ type: row.type, channel, enabled: e.target.checked })}
aria-label={`${row.label} via ${channel}`}
/>
))}
))}
);
}
Checklist