miles990

communication-systems

Email, notifications, and messaging system patterns

miles990 20 6 Updated 8mo ago

Resources

1
GitHub

Install

npx skillscat add miles990/claude-software-skills/communication-systems

Install via the SkillsCat registry.

About this skill

This skill provides patterns for building email systems, push notifications, in-app messaging, and webhook integrations using services like Resend and React Email. It addresses the challenge of implementing reliable communication features by offering reusable code structures for sending transactional emails, managing templates, and tracking delivery status. Developers should use this when implementing notification systems that require sending emails, alerts, or messages across multiple channels.

SKILL.md

Communication Systems

Overview

Building email systems, push notifications, in-app messaging, and webhook integrations.


Email Systems

Transactional Email

import { Resend } from 'resend';

const resend = new Resend(process.env.RESEND_API_KEY);

interface EmailOptions {
  to: string | string[];
  subject: string;
  html?: string;
  text?: string;
  template?: string;
  data?: Record<string, any>;
  attachments?: Array<{
    filename: string;
    content: Buffer | string;
  }>;
}

async function sendEmail(options: EmailOptions) {
  let html = options.html;

  // Use template if specified
  if (options.template) {
    html = await renderTemplate(options.template, options.data);
  }

  const { data, error } = await resend.emails.send({
    from: 'noreply@example.com',
    to: options.to,
    subject: options.subject,
    html,
    text: options.text,
    attachments: options.attachments,
  });

  if (error) {
    console.error('Email send failed:', error);
    throw error;
  }

  // Log for tracking
  await prisma.emailLog.create({
    data: {
      messageId: data.id,
      to: Array.isArray(options.to) ? options.to.join(',') : options.to,
      subject: options.subject,
      template: options.template,
      status: 'sent',
    },
  });

  return data;
}

// Email templates with React Email
import { render } from '@react-email/render';
import { WelcomeEmail } from './templates/WelcomeEmail';
import { PasswordResetEmail } from './templates/PasswordResetEmail';

const templates = {
  welcome: WelcomeEmail,
  passwordReset: PasswordResetEmail,
};

async function renderTemplate(name: string, data: Record<string, any>) {
  const Template = templates[name];
  if (!Template) throw new Error(`Template ${name} not found`);

  return render(<Template {...data} />);
}

// React Email template
import {
  Html, Head, Body, Container, Text, Button, Img,
} from '@react-email/components';

function WelcomeEmail({ name, actionUrl }: { name: string; actionUrl: string }) {
  return (
    <Html>
      <Head />
      <Body style={{ fontFamily: 'Arial, sans-serif' }}>
        <Container>
          <Img src="https://example.com/logo.png" width="120" height="40" alt="Logo" />
          <Text>Hi {name},</Text>
          <Text>Welcome to our platform! Get started by setting up your account.</Text>
          <Button
            href={actionUrl}
            style={{ background: '#007bff', color: '#fff', padding: '12px 24px' }}
          >
            Get Started
          </Button>
        </Container>
      </Body>
    </Html>
  );
}

Email Queue

import Bull from 'bull';

const emailQueue = new Bull('email', process.env.REDIS_URL);

// Add to queue
async function queueEmail(options: EmailOptions, delay?: number) {
  return emailQueue.add('send', options, {
    delay,
    attempts: 3,
    backoff: { type: 'exponential', delay: 60000 },
  });
}

// Process queue
emailQueue.process('send', async (job) => {
  await sendEmail(job.data);
});

// Handle failures
emailQueue.on('failed', async (job, error) => {
  console.error(`Email job ${job.id} failed:`, error);

  await prisma.emailLog.update({
    where: { jobId: job.id },
    data: { status: 'failed', error: error.message },
  });
});

// Bulk email with rate limiting
async function sendBulkEmail(recipients: string[], template: string, data: Record<string, any>) {
  const jobs = recipients.map((to, index) => ({
    name: 'send',
    data: { to, template, data },
    opts: { delay: index * 100 }, // Stagger sends
  }));

  await emailQueue.addBulk(jobs);
}

Push Notifications

Web Push

import webpush from 'web-push';

webpush.setVapidDetails(
  'mailto:admin@example.com',
  process.env.VAPID_PUBLIC_KEY!,
  process.env.VAPID_PRIVATE_KEY!
);

interface PushSubscription {
  endpoint: string;
  keys: {
    p256dh: string;
    auth: string;
  };
}

// Store subscription
async function saveSubscription(userId: string, subscription: PushSubscription) {
  await prisma.pushSubscription.upsert({
    where: { endpoint: subscription.endpoint },
    update: { keys: subscription.keys },
    create: {
      userId,
      endpoint: subscription.endpoint,
      keys: subscription.keys,
    },
  });
}

// Send push notification
async function sendPush(userId: string, payload: {
  title: string;
  body: string;
  icon?: string;
  url?: string;
  data?: Record<string, any>;
}) {
  const subscriptions = await prisma.pushSubscription.findMany({
    where: { userId },
  });

  const results = await Promise.allSettled(
    subscriptions.map(async (sub) => {
      try {
        await webpush.sendNotification(
          { endpoint: sub.endpoint, keys: sub.keys },
          JSON.stringify(payload)
        );
      } catch (error) {
        if (error.statusCode === 410) {
          // Subscription expired, remove it
          await prisma.pushSubscription.delete({ where: { id: sub.id } });
        }
        throw error;
      }
    })
  );

  return results;
}

// Service worker handler
// public/sw.js
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',
      data: data,
    })
  );
});

self.addEventListener('notificationclick', (event) => {
  event.notification.close();

  if (event.notification.data.url) {
    event.waitUntil(clients.openWindow(event.notification.data.url));
  }
});

Mobile Push (FCM)

import admin from 'firebase-admin';

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
});

interface MobileNotification {
  title: string;
  body: string;
  imageUrl?: string;
  data?: Record<string, string>;
}

async function sendMobilePush(
  tokens: string[],
  notification: MobileNotification
) {
  const message: admin.messaging.MulticastMessage = {
    tokens,
    notification: {
      title: notification.title,
      body: notification.body,
      imageUrl: notification.imageUrl,
    },
    data: notification.data,
    android: {
      priority: 'high',
      notification: {
        sound: 'default',
        clickAction: 'OPEN_ACTIVITY',
      },
    },
    apns: {
      payload: {
        aps: {
          sound: 'default',
          badge: 1,
        },
      },
    },
  };

  const response = await admin.messaging().sendEachForMulticast(message);

  // Handle failures
  response.responses.forEach((resp, idx) => {
    if (!resp.success) {
      const errorCode = resp.error?.code;
      if (
        errorCode === 'messaging/invalid-registration-token' ||
        errorCode === 'messaging/registration-token-not-registered'
      ) {
        // Remove invalid token
        removeDeviceToken(tokens[idx]);
      }
    }
  });

  return response;
}

In-App Notifications

interface Notification {
  id: string;
  userId: string;
  type: string;
  title: string;
  message: string;
  data?: Record<string, any>;
  read: boolean;
  createdAt: Date;
}

// Create notification
async function createNotification(params: {
  userId: string;
  type: string;
  title: string;
  message: string;
  data?: Record<string, any>;
}) {
  const notification = await prisma.notification.create({
    data: {
      ...params,
      read: false,
    },
  });

  // Send real-time update
  await pubsub.publish(`notifications:${params.userId}`, {
    type: 'NEW_NOTIFICATION',
    notification,
  });

  return notification;
}

// Get notifications with pagination
async function getNotifications(userId: string, options: {
  page?: number;
  limit?: number;
  unreadOnly?: boolean;
}) {
  const { page = 1, limit = 20, unreadOnly = false } = options;

  const where = {
    userId,
    ...(unreadOnly && { read: false }),
  };

  const [notifications, total, unreadCount] = await Promise.all([
    prisma.notification.findMany({
      where,
      orderBy: { createdAt: 'desc' },
      skip: (page - 1) * limit,
      take: limit,
    }),
    prisma.notification.count({ where }),
    prisma.notification.count({ where: { userId, read: false } }),
  ]);

  return { notifications, total, unreadCount };
}

// Mark as read
async function markAsRead(userId: string, notificationIds: string[]) {
  await prisma.notification.updateMany({
    where: {
      id: { in: notificationIds },
      userId,
    },
    data: { read: true },
  });
}

// React hook for notifications
function useNotifications() {
  const [notifications, setNotifications] = useState<Notification[]>([]);
  const [unreadCount, setUnreadCount] = useState(0);

  useEffect(() => {
    // Initial fetch
    fetchNotifications().then(({ notifications, unreadCount }) => {
      setNotifications(notifications);
      setUnreadCount(unreadCount);
    });

    // Subscribe to real-time updates
    const unsubscribe = subscribeToNotifications((notification) => {
      setNotifications((prev) => [notification, ...prev]);
      setUnreadCount((prev) => prev + 1);
    });

    return unsubscribe;
  }, []);

  return { notifications, unreadCount, markAsRead };
}

Webhooks

interface Webhook {
  id: string;
  url: string;
  secret: string;
  events: string[];
  active: boolean;
}

// Register webhook
async function registerWebhook(params: {
  url: string;
  events: string[];
}) {
  const secret = crypto.randomBytes(32).toString('hex');

  return prisma.webhook.create({
    data: {
      url: params.url,
      events: params.events,
      secret,
      active: true,
    },
  });
}

// Send webhook
async function sendWebhook(webhookId: string, event: string, payload: any) {
  const webhook = await prisma.webhook.findUnique({ where: { id: webhookId } });
  if (!webhook || !webhook.active) return;

  const timestamp = Date.now().toString();
  const body = JSON.stringify({ event, data: payload, timestamp });

  // Create signature
  const signature = crypto
    .createHmac('sha256', webhook.secret)
    .update(body)
    .digest('hex');

  try {
    const response = await fetch(webhook.url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Webhook-Signature': signature,
        'X-Webhook-Timestamp': timestamp,
      },
      body,
    });

    await prisma.webhookLog.create({
      data: {
        webhookId,
        event,
        payload,
        responseStatus: response.status,
        success: response.ok,
      },
    });

    // Disable after multiple failures
    if (!response.ok) {
      await handleWebhookFailure(webhookId);
    }
  } catch (error) {
    await prisma.webhookLog.create({
      data: {
        webhookId,
        event,
        payload,
        error: error.message,
        success: false,
      },
    });

    await handleWebhookFailure(webhookId);
  }
}

// Verify webhook signature (receiver side)
function verifyWebhookSignature(
  body: string,
  signature: string,
  secret: string
): boolean {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

Related Skills

  • [[realtime-systems]] - Real-time messaging
  • [[backend]] - API development
  • [[reliability-engineering]] - Delivery guarantees