Notification systems span three channels — email, push, and in-app — each with different delivery guarantees, opt-in requirements, and user fatigue risk. Claude Code generates the notification service layer, React Email templates, web push subscription management, and the preference system that lets users control what they receive.
Notification Service Architecture
Build a unified notification service.
Channels: email, web push, in-app. Each can be enabled/disabled per user.
Types: order updates, marketing, security alerts. Security alerts bypass opt-outs.
// services/NotificationService.ts
import { db } from '../db';
import { EmailChannel } from './channels/EmailChannel';
import { PushChannel } from './channels/PushChannel';
import { InAppChannel } from './channels/InAppChannel';
type NotificationType = 'order_update' | 'marketing' | 'security_alert' | 'system';
type Channel = 'email' | 'push' | 'in_app';
interface Notification {
userId: string;
type: NotificationType;
templateId: string;
data: Record<string, unknown>;
channels?: Channel[]; // Override — defaults to user preference + type defaults
}
const TYPE_DEFAULT_CHANNELS: Record<NotificationType, Channel[]> = {
order_update: ['email', 'push', 'in_app'],
marketing: ['email'],
security_alert: ['email', 'push', 'in_app'], // Cannot be disabled
system: ['in_app'],
};
export class NotificationService {
constructor(
private email: EmailChannel,
private push: PushChannel,
private inApp: InAppChannel,
) {}
async send(notification: Notification): Promise<void> {
const channels = notification.channels ?? TYPE_DEFAULT_CHANNELS[notification.type];
const enabledChannels = await this.filterByPreferences(
notification.userId,
notification.type,
channels,
);
const results = await Promise.allSettled(
enabledChannels.map(channel => this.sendToChannel(channel, notification))
);
// Log delivery results
await db('notification_log').insert({
user_id: notification.userId,
type: notification.type,
template_id: notification.templateId,
channels_attempted: enabledChannels,
channels_succeeded: enabledChannels.filter((_, i) => results[i].status === 'fulfilled'),
sent_at: new Date(),
});
}
private async filterByPreferences(
userId: string,
type: NotificationType,
channels: Channel[],
): Promise<Channel[]> {
// Security alerts are mandatory — bypass preferences
if (type === 'security_alert') return channels;
const prefs = await db('notification_preferences')
.where('user_id', userId)
.first();
if (!prefs) return channels; // Default: all channels enabled
return channels.filter(channel => prefs[`${channel}_${type}`] !== false);
}
private async sendToChannel(channel: Channel, notification: Notification): Promise<void> {
switch (channel) {
case 'email': return this.email.send(notification);
case 'push': return this.push.send(notification);
case 'in_app': return this.inApp.send(notification);
}
}
}
React Email Templates
Create transactional email templates using React Email.
Need: order confirmation, password reset, trial ending.
// emails/OrderConfirmation.tsx
import {
Body, Button, Container, Head, Heading, Hr, Html,
Img, Link, Preview, Row, Column, Section, Text,
} from '@react-email/components';
interface OrderConfirmationProps {
customerName: string;
orderId: string;
items: { name: string; quantity: number; priceCents: number }[];
totalCents: number;
trackingUrl?: string;
}
export function OrderConfirmation({
customerName,
orderId,
items,
totalCents,
trackingUrl,
}: OrderConfirmationProps) {
return (
<Html>
<Head />
<Preview>Your order #{orderId} is confirmed — thank you!</Preview>
<Body style={{ backgroundColor: '#f6f9fc', fontFamily: '-apple-system,sans-serif' }}>
<Container style={{ maxWidth: '600px', margin: '40px auto', backgroundColor: '#fff', borderRadius: '8px', padding: '40px' }}>
<Heading style={{ fontSize: '24px', color: '#1a1a1a', marginBottom: '8px' }}>
Order Confirmed
</Heading>
<Text style={{ color: '#4a5568', marginBottom: '24px' }}>
Hi {customerName}, your order #{orderId} has been confirmed.
</Text>
<Section>
{items.map((item, i) => (
<Row key={i} style={{ marginBottom: '12px' }}>
<Column style={{ flex: 1 }}>
<Text style={{ margin: 0, fontWeight: '600' }}>{item.name}</Text>
<Text style={{ margin: 0, color: '#718096', fontSize: '14px' }}>
Qty: {item.quantity}
</Text>
</Column>
<Column style={{ textAlign: 'right' }}>
<Text style={{ margin: 0 }}>
${((item.priceCents * item.quantity) / 100).toFixed(2)}
</Text>
</Column>
</Row>
))}
</Section>
<Hr />
<Row>
<Column><Text style={{ fontWeight: '700', margin: 0 }}>Total</Text></Column>
<Column style={{ textAlign: 'right' }}>
<Text style={{ fontWeight: '700', margin: 0 }}>${(totalCents / 100).toFixed(2)}</Text>
</Column>
</Row>
{trackingUrl && (
<>
<Hr />
<Button href={trackingUrl} style={{ backgroundColor: '#2563eb', color: '#fff', padding: '12px 24px', borderRadius: '6px', textDecoration: 'none', display: 'inline-block', marginTop: '16px' }}>
Track Your Order
</Button>
</>
)}
<Hr />
<Text style={{ fontSize: '12px', color: '#718096', textAlign: 'center' }}>
You're receiving this because you placed an order at example.com.{' '}
<Link href="{{{unsubscribe_url}}}">Manage preferences</Link>
</Text>
</Container>
</Body>
</Html>
);
}
// Send with Resend
import { Resend } from 'resend';
import { render } from '@react-email/render';
import { OrderConfirmation } from '../emails/OrderConfirmation';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function sendOrderConfirmation(to: string, props: OrderConfirmationProps) {
const html = render(<OrderConfirmation {...props} />);
const text = render(<OrderConfirmation {...props} />, { plainText: true });
await resend.emails.send({
from: '[email protected]',
to,
subject: `Order #${props.orderId} confirmed`,
html,
text,
});
}
Web Push Notifications
Add web push notifications for order status updates.
Show browser permission request after first purchase.
// client/usePushNotifications.ts
export function usePushNotifications() {
const subscribe = async () => {
if (!('PushManager' in window)) return null;
const permission = await Notification.requestPermission();
if (permission !== 'granted') return null;
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!),
});
// Send subscription to server
await fetch('/api/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription.toJSON()),
});
return subscription;
};
return { subscribe };
}
function urlBase64ToUint8Array(base64String: string) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const rawData = window.atob(base64);
return Uint8Array.from([...rawData].map(char => char.charCodeAt(0)));
}
// server: send push notification
import webpush from 'web-push';
webpush.setVapidDetails(
'mailto:[email protected]',
process.env.VAPID_PUBLIC_KEY!,
process.env.VAPID_PRIVATE_KEY!,
);
export async function sendPushNotification(userId: string, payload: {
title: string;
body: string;
url: string;
icon?: string;
}) {
const subscriptions = await db('push_subscriptions').where('user_id', userId);
await Promise.allSettled(
subscriptions.map(sub =>
webpush.sendNotification(
{ endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } },
JSON.stringify(payload),
).catch(async (error) => {
// Remove expired subscriptions (410 Gone)
if (error.statusCode === 410) {
await db('push_subscriptions').where('id', sub.id).delete();
}
})
)
);
}
// public/sw.js — service worker handles push events
self.addEventListener('push', (event) => {
const data = event.data?.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: data.icon ?? '/icon-192.png',
badge: '/badge-72.png',
data: { url: data.url },
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(clients.openWindow(event.notification.data.url));
});
For the drip email sequences that use these notification templates, see the team workflow guide. For the PWA service worker setup that enables web push, see the PWA guide. The Claude Skills 360 bundle includes notification system skill sets for React Email, web push, and multi-channel preference management. Start with the free tier to try notification system scaffolding.