Claude Code for Notification Systems: Email, Push, and In-App Notifications — Claude Skills 360 Blog
Blog / Development / Claude Code for Notification Systems: Email, Push, and In-App Notifications
Development

Claude Code for Notification Systems: Email, Push, and In-App Notifications

Published: August 2, 2026
Read time: 8 min read
By: Claude Skills 360

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.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free