Notification Examples

Comprehensive examples for implementing notifications with the Asterisms JS SDK Backend.

Overview

This guide provides detailed examples of notification implementation using the Asterisms JS SDK Backend. It covers various notification scenarios, from simple user notifications to complex multi-channel notification systems.

Basic Notification Sending

Simple User Notification

// Basic notification to a user
import { getAsterismsBackendSDK, createNotification } from '@asterisms/sdk-backend';

const sdk = getAsterismsBackendSDK(props);
await sdk.boot();

const notifications = sdk.notifications();

async function sendWelcomeNotification(accountId: string) {
  try {
    const notification = createNotification({
      sender: 'your-app',
      subject: 'Welcome to Our Platform!',
      text: 'Thanks for joining us. Get started by exploring your dashboard.',
      urgency: 'NONE'
    });

    await notifications.sendToAccount(accountId, notification);

    console.log('Welcome notification sent successfully');
  } catch (error) {
    console.error('Failed to send welcome notification:', error);
  }
}

Workspace Notification

// Send notification to all users in a workspace
async function sendWorkspaceAnnouncement(workspaceId: string) {
  const notifications = sdk.notifications();

  try {
    const notification = createNotification({
      sender: 'your-app',
      subject: 'System Maintenance Notice',
      text: 'Scheduled maintenance will occur tonight from 2 AM to 4 AM EST.',
      html: '<p>Scheduled maintenance will occur tonight from <strong>2 AM to 4 AM EST</strong>.</p>',
      urgency: 'IMPORTANT'
    });

    await notifications.sendToWorkspace(workspaceId, notification);

    console.log('Workspace announcement sent successfully');
  } catch (error) {
    console.error('Failed to send workspace announcement:', error);
  }
}

SvelteKit Notification API

Send Notification Route

// src/routes/api/notifications/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const POST: RequestHandler = async ({ request, locals }) => {
  try {
    const { userId, workspaceId, notification } = await request.json();

    if (!notification || !notification.subject || !notification.text) {
      return json({ error: 'Notification subject and text are required' }, { status: 400 });
    }

    const sdk = locals.sdk;
    if (!sdk) {
      return json({ error: 'SDK not available' }, { status: 503 });
    }

    const notifications = sdk.notifications();

    if (userId) {
      const notificationPayload = createNotification({
        sender: 'your-app',
        subject: notification.subject,
        text: notification.text,
        html: notification.html,
        urgency: notification.urgency || 'NONE'
      });
      await notifications.sendToAccount(userId, notificationPayload);
    } else if (workspaceId) {
      const notificationPayload = createNotification({
        sender: 'your-app',
        subject: notification.subject,
        text: notification.text,
        html: notification.html,
        urgency: notification.urgency || 'NONE'
      });
      await notifications.sendToWorkspace(workspaceId, notificationPayload);
    } else {
      return json({ error: 'Either userId or workspaceId must be provided' }, { status: 400 });
    }

    return json({ success: true });
  } catch (error) {
    console.error('Notification sending error:', error);
    return json({ error: 'Failed to send notification' }, { status: 500 });
  }
};

Bulk Notifications

// src/routes/api/notifications/bulk/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

interface BulkNotificationRequest {
  notifications: Array<{
    userId: string;
    subject: string;
    text: string;
    html?: string;
    urgency?: 'NONE' | 'IMPORTANT' | 'ERROR' | 'CRITICAL';
  }>;
}

export const POST: RequestHandler = async ({ request, locals }) => {
  try {
    const { notifications: notificationsData } = (await request.json()) as BulkNotificationRequest;

    if (!Array.isArray(notificationsData) || notificationsData.length === 0) {
      return json({ error: 'Notifications array is required' }, { status: 400 });
    }

    const sdk = locals.sdk;
    if (!sdk) {
      return json({ error: 'SDK not available' }, { status: 503 });
    }

    const notifications = sdk.notifications();

    // Send notifications in batches to avoid overwhelming the system
    const batchSize = 100;
    const results = [];

    for (let i = 0; i < notificationsData.length; i += batchSize) {
      const batch = notificationsData.slice(i, i + batchSize);

      const batchPromises = batch.map(async (notif) => {
        try {
          const notification = createNotification({
            sender: 'your-app',
            subject: notif.subject,
            text: notif.text,
            html: notif.html,
            urgency: notif.urgency || 'NONE'
          });
          await notifications.sendToAccount(notif.userId, notification);
          return { userId: notif.userId, success: true };
        } catch (error) {
          return { userId: notif.userId, success: false, error: error.message };
        }
      });

      const batchResults = await Promise.all(batchPromises);
      results.push(...batchResults);
    }

    const successCount = results.filter((r) => r.success).length;
    const failureCount = results.filter((r) => !r.success).length;

    return json({
      success: true,
      results: {
        total: notificationsData.length,
        successful: successCount,
        failed: failureCount,
        details: results
      }
    });
  } catch (error) {
    console.error('Bulk notification error:', error);
    return json({ error: 'Failed to send bulk notifications' }, { status: 500 });
  }
};

Advanced Notification Patterns

Notification Templates

// src/lib/notifications/templates.ts
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';

export class NotificationTemplates {
  constructor(private sdk: AsterismsBackendSDK) {}
  async sendUserRegistrationNotification(accountId: string, userName: string) {
    const notifications = this.sdk.notifications();

    const notification = createNotification({
      sender: 'your-app',
      subject: `Welcome to Our Platform, ${userName}!`,
      text: 'Your account has been created successfully. Start exploring our features.',
      html: `<p>Your account has been created successfully. <a href="/dashboard">Start exploring our features</a>.</p>`,
      urgency: 'NONE'
    });

    await notifications.sendToAccount(accountId, notification);
  }
  async sendPasswordResetNotification(accountId: string, resetToken: string) {
    const notifications = this.sdk.notifications();

    const notification = createNotification({
      sender: 'your-app',
      subject: 'Password Reset Request',
      text: `Click the link below to reset your password. This link expires in 1 hour. ${process.env.APP_URL}/reset-password?token=${resetToken}`,
      html: `<p>Click the link below to reset your password. This link expires in 1 hour.</p><p><a href="${process.env.APP_URL}/reset-password?token=${resetToken}">Reset Password</a></p>`,
      urgency: 'IMPORTANT'
    });

    await notifications.sendToAccount(accountId, notification);
  }
  async sendOrderConfirmationNotification(accountId: string, orderId: string, amount: number) {
    const notifications = this.sdk.notifications();

    const notification = createNotification({
      sender: 'your-app',
      subject: 'Order Confirmation',
      text: `Your order #${orderId} has been confirmed. Total: $${amount.toFixed(2)}. View order details at ${process.env.APP_URL}/orders/${orderId}`,
      html: `<p>Your order #${orderId} has been confirmed. Total: $${amount.toFixed(2)}</p><p><a href="${process.env.APP_URL}/orders/${orderId}">View Order</a></p>`,
      urgency: 'NONE'
    });

    await notifications.sendToAccount(accountId, notification);
  }
  async sendSystemMaintenanceNotification(workspaceId: string, startTime: string, endTime: string) {
    const notifications = this.sdk.notifications();

    const notification = createNotification({
      sender: 'your-app',
      subject: 'Scheduled System Maintenance',
      text: `System maintenance is scheduled from ${startTime} to ${endTime}. Some services may be temporarily unavailable. Learn more at ${process.env.APP_URL}/maintenance-info`,
      html: `<p>System maintenance is scheduled from ${startTime} to ${endTime}. Some services may be temporarily unavailable.</p><p><a href="${process.env.APP_URL}/maintenance-info">Learn More</a></p>`,
      urgency: 'IMPORTANT'
    });

    await notifications.sendToWorkspace(workspaceId, notification);
  }
  async sendSecurityAlertNotification(accountId: string, event: string, location: string) {
    const notifications = this.sdk.notifications();

    const notification = createNotification({
      sender: 'your-app',
      subject: 'Security Alert',
      text: `${event} detected from ${location}. If this wasn't you, please secure your account immediately. Review security settings at ${process.env.APP_URL}/security-settings`,
      html: `<p>${event} detected from ${location}. If this wasn't you, please secure your account immediately.</p><p><a href="${process.env.APP_URL}/security-settings">Review Security</a></p>`,
      urgency: 'CRITICAL'
    });

    await notifications.sendToAccount(accountId, notification);
  }
}

Scheduled Notifications

The notification service has built-in queueing and scheduling capabilities through the delivery directive. Here's how to use it:

// src/lib/notifications/scheduled.ts
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';

export class ScheduledNotifications {
  constructor(private sdk: AsterismsBackendSDK) {}

  async scheduleNotification(
    accountId: string,
    notification: {
      subject: string;
      text: string;
      html?: string;
      urgency?: 'NONE' | 'IMPORTANT' | 'ERROR' | 'CRITICAL';
    },
    scheduleFor: Date
  ): Promise<void> {
    const notifications = this.sdk.notifications();

    const notificationPayload = createNotification({
      sender: 'your-app',
      subject: notification.subject,
      text: notification.text,
      html: notification.html,
      urgency: notification.urgency || 'NONE'
    });

    // Calculate delay from now until the scheduled time
    const delayMs = scheduleFor.getTime() - Date.now();
    const delaySeconds = Math.max(0, Math.floor(delayMs / 1000));

    // Use the delivery directive to schedule the notification
    // The delay field uses Java Duration format (ISO-8601 duration format)
    const notificationWithDelivery = {
      ...notificationPayload,
      delivery: {
        schedule: 'WITH_DELAY' as const,
        delay: `PT${delaySeconds}S` // ISO-8601 duration format: PT<seconds>S
      }
    };

    await notifications.sendToAccount(accountId, notificationWithDelivery);
  }

  async scheduleWorkspaceNotification(
    workspaceId: string,
    notification: {
      subject: string;
      text: string;
      html?: string;
      urgency?: 'NONE' | 'IMPORTANT' | 'ERROR' | 'CRITICAL';
    },
    scheduleFor: Date
  ): Promise<void> {
    const notifications = this.sdk.notifications();

    const notificationPayload = createNotification({
      sender: 'your-app',
      subject: notification.subject,
      text: notification.text,
      html: notification.html,
      urgency: notification.urgency || 'NONE'
    });

    // Calculate delay from now until the scheduled time
    const delayMs = scheduleFor.getTime() - Date.now();
    const delaySeconds = Math.max(0, Math.floor(delayMs / 1000));

    // Use the delivery directive to schedule the notification
    // The delay field uses Java Duration format (ISO-8601 duration format)
    const notificationWithDelivery = {
      ...notificationPayload,
      delivery: {
        schedule: 'WITH_DELAY' as const,
        delay: `PT${delaySeconds}S` // ISO-8601 duration format: PT<seconds>S
      }
    };

    await notifications.sendToWorkspace(workspaceId, notificationWithDelivery);
  }

  // Helper method for common delay patterns
  async scheduleWithDelay(
    accountId: string,
    notification: {
      subject: string;
      text: string;
      html?: string;
      urgency?: 'NONE' | 'IMPORTANT' | 'ERROR' | 'CRITICAL';
    },
    delay: {
      seconds?: number;
      minutes?: number;
      hours?: number;
      days?: number;
    }
  ): Promise<void> {
    const notifications = this.sdk.notifications();

    const notificationPayload = createNotification({
      sender: 'your-app',
      subject: notification.subject,
      text: notification.text,
      html: notification.html,
      urgency: notification.urgency || 'NONE'
    });

    // Build ISO-8601 duration string
    const parts = [];
    if (delay.days) parts.push(`${delay.days}D`);

    const timeParts = [];
    if (delay.hours) timeParts.push(`${delay.hours}H`);
    if (delay.minutes) timeParts.push(`${delay.minutes}M`);
    if (delay.seconds) timeParts.push(`${delay.seconds}S`);

    let durationString = 'P';
    if (parts.length > 0) durationString += parts.join('');
    if (timeParts.length > 0) durationString += 'T' + timeParts.join('');

    // If no components specified, default to 0 seconds
    if (durationString === 'P') durationString = 'PT0S';

    const notificationWithDelivery = {
      ...notificationPayload,
      delivery: {
        schedule: 'WITH_DELAY' as const,
        delay: durationString // ISO-8601 duration format: P[n]DT[n]H[n]M[n]S
      }
    };

    await notifications.sendToAccount(accountId, notificationWithDelivery);
  }
}

Real-time Notifications

WebSocket Notification Service

// src/lib/notifications/websocket.ts
import WebSocket from 'ws';
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';

interface NotificationClient {
  userId: string;
  ws: WebSocket;
  subscriptions: Set<string>;
}

export class WebSocketNotificationService {
  private clients = new Map<string, NotificationClient>();

  constructor(private sdk: AsterismsBackendSDK) {}

  addClient(userId: string, ws: WebSocket): void {
    const client: NotificationClient = {
      userId,
      ws,
      subscriptions: new Set()
    };

    this.clients.set(userId, client);

    ws.on('message', (message) => {
      this.handleClientMessage(userId, message.toString());
    });

    ws.on('close', () => {
      this.clients.delete(userId);
    });

    // Send welcome message
    this.sendToClient(userId, {
      type: 'connection',
      message: 'Connected to notification service'
    });
  }

  private handleClientMessage(userId: string, message: string): void {
    try {
      const data = JSON.parse(message);
      const client = this.clients.get(userId);

      if (!client) return;

      switch (data.type) {
        case 'subscribe':
          client.subscriptions.add(data.channel);
          this.sendToClient(userId, {
            type: 'subscribed',
            channel: data.channel
          });
          break;
        case 'unsubscribe':
          client.subscriptions.delete(data.channel);
          this.sendToClient(userId, {
            type: 'unsubscribed',
            channel: data.channel
          });
          break;
      }
    } catch (error) {
      console.error('Invalid message from client:', error);
    }
  }

  private sendToClient(userId: string, message: any): void {
    const client = this.clients.get(userId);
    if (client && client.ws.readyState === WebSocket.OPEN) {
      client.ws.send(JSON.stringify(message));
    }
  }

  async sendNotificationToUser(accountId: string, notification: any): Promise<void> {
    // Send via WebSocket if client is connected
    this.sendToClient(accountId, {
      type: 'notification',
      notification
    });

    // Also send via regular notification service
    const notifications = this.sdk.notifications();
    const notificationPayload = createNotification({
      sender: 'your-app',
      subject: notification.subject,
      text: notification.text,
      html: notification.html,
      urgency: notification.urgency || 'NONE'
    });
    await notifications.sendToAccount(accountId, notificationPayload);
  }
  async sendNotificationToWorkspace(workspaceId: string, notification: any): Promise<void> {
    // Send via WebSocket to all connected clients in workspace
    const storage = this.sdk.storage();
    const workspaceFiles = await storage.list({ bucketId: workspaceId });

    for (const user of workspaceUsers) {
      this.sendToClient(user.accessorId, {
        type: 'notification',
        notification
      });
    }

    // Also send via regular notification service
    const notifications = this.sdk.notifications();
    const notificationPayload = createNotification({
      sender: 'your-app',
      subject: notification.subject,
      text: notification.text,
      html: notification.html,
      urgency: notification.urgency || 'NONE'
    });
    await notifications.sendToWorkspace(workspaceId, notificationPayload);
  }

  broadcastToChannel(channel: string, message: any): void {
    this.clients.forEach((client) => {
      if (client.subscriptions.has(channel)) {
        this.sendToClient(client.userId, {
          type: 'channel_message',
          channel,
          message
        });
      }
    });
  }
}

Email Notifications

Email Notification Service

// src/lib/notifications/email.ts
import nodemailer from 'nodemailer';
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';

interface EmailNotification {
  to: string;
  subject: string;
  html: string;
  text?: string;
}

export class EmailNotificationService {
  private transporter: nodemailer.Transporter;

  constructor(private sdk: AsterismsBackendSDK) {
    this.transporter = nodemailer.createTransporter({
      host: process.env.SMTP_HOST,
      port: parseInt(process.env.SMTP_PORT || '587'),
      secure: process.env.SMTP_SECURE === 'true',
      auth: {
        user: process.env.SMTP_USER,
        pass: process.env.SMTP_PASS
      }
    });
  }

  async sendEmail(notification: EmailNotification): Promise<void> {
    try {
      await this.transporter.sendMail({
        from: process.env.FROM_EMAIL,
        to: notification.to,
        subject: notification.subject,
        html: notification.html,
        text: notification.text
      });

      console.log(`Email sent to ${notification.to}`);
    } catch (error) {
      console.error('Email sending failed:', error);
      throw error;
    }
  }

  async sendWelcomeEmail(userEmail: string, userName: string): Promise<void> {
    const html = `
      <h1>Welcome to Our Platform, ${userName}!</h1>
      <p>Thank you for joining us. We're excited to have you on board.</p>
      <p>Get started by visiting your dashboard:</p>
      <a href="${process.env.APP_URL}/dashboard" style="background: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Go to Dashboard</a>
    `;

    await this.sendEmail({
      to: userEmail,
      subject: 'Welcome to Our Platform!',
      html,
      text: `Welcome to Our Platform, ${userName}! Visit ${process.env.APP_URL}/dashboard to get started.`
    });
  }

  async sendPasswordResetEmail(userEmail: string, resetToken: string): Promise<void> {
    const resetUrl = `${process.env.APP_URL}/reset-password?token=${resetToken}`;

    const html = `
      <h1>Password Reset Request</h1>
      <p>You requested a password reset. Click the link below to reset your password:</p>
      <a href="${resetUrl}" style="background: #dc3545; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Reset Password</a>
      <p>This link expires in 1 hour.</p>
      <p>If you didn't request this, please ignore this email.</p>
    `;

    await this.sendEmail({
      to: userEmail,
      subject: 'Password Reset Request',
      html,
      text: `Password reset requested. Visit ${resetUrl} to reset your password. This link expires in 1 hour.`
    });
  }
}

Notification Preferences

User Preference Management

// src/lib/notifications/preferences.ts
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';

interface NotificationPreferences {
  userId: string;
  email: {
    enabled: boolean;
    types: string[];
  };
  push: {
    enabled: boolean;
    types: string[];
  };
  inApp: {
    enabled: boolean;
    types: string[];
  };
  frequency: 'immediate' | 'daily' | 'weekly';
}

export class NotificationPreferencesService {
  constructor(private sdk: AsterismsBackendSDK) {}

  async getUserPreferences(userId: string): Promise<NotificationPreferences> {
    const storage = this.sdk.storage();
    const preferencesFile = await storage.getData('notification_preferences_' + userId);

    if (!preferences) {
      // Return default preferences
      return {
        userId,
        email: {
          enabled: true,
          types: ['security', 'system', 'updates']
        },
        push: {
          enabled: true,
          types: ['messages', 'security']
        },
        inApp: {
          enabled: true,
          types: ['all']
        },
        frequency: 'immediate'
      };
    }

    return preferences;
  }

  async updateUserPreferences(
    userId: string,
    preferences: Partial<NotificationPreferences>
  ): Promise<void> {
    const storage = this.sdk.storage();
    const currentPreferences = await this.getUserPreferences(userId);

    const updatedPreferences = {
      ...currentPreferences,
      ...preferences,
      userId
    };

    await storage.upload(
      new File([JSON.stringify(updatedPreferences)], 'notification_preferences_' + userId, {
        type: 'application/json'
      })
    );
  }

  async shouldSendNotification(
    userId: string,
    notificationType: string,
    channel: 'email' | 'push' | 'inApp'
  ): Promise<boolean> {
    const preferences = await this.getUserPreferences(userId);
    const channelPrefs = preferences[channel];

    return (
      channelPrefs.enabled &&
      (channelPrefs.types.includes('all') || channelPrefs.types.includes(notificationType))
    );
  }
}

Preference API Routes

// src/routes/api/notifications/preferences/+server.ts
import { json } from '@sveltejs/kit';
import { NotificationPreferencesService } from '$lib/notifications/preferences';
import type { RequestHandler } from './$types';

export const GET: RequestHandler = async ({ locals, url }) => {
  try {
    const user = locals.user;
    if (!user) {
      return json({ error: 'Unauthorized' }, { status: 401 });
    }

    const sdk = locals.sdk;
    const preferencesService = new NotificationPreferencesService(sdk);
    const preferences = await preferencesService.getUserPreferences(user.id);

    return json({ preferences });
  } catch (error) {
    console.error('Get preferences error:', error);
    return json({ error: 'Failed to get preferences' }, { status: 500 });
  }
};

export const PUT: RequestHandler = async ({ request, locals }) => {
  try {
    const user = locals.user;
    if (!user) {
      return json({ error: 'Unauthorized' }, { status: 401 });
    }

    const preferences = await request.json();
    const sdk = locals.sdk;
    const preferencesService = new NotificationPreferencesService(sdk);

    await preferencesService.updateUserPreferences(user.id, preferences);

    return json({ success: true });
  } catch (error) {
    console.error('Update preferences error:', error);
    return json({ error: 'Failed to update preferences' }, { status: 500 });
  }
};

Push Notifications

Push Notification Service

// src/lib/notifications/push.ts
import webpush from 'web-push';
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';

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

export class PushNotificationService {
  constructor(private sdk: AsterismsBackendSDK) {
    webpush.setVapidDetails(
      process.env.VAPID_SUBJECT || 'mailto:admin@example.com',
      process.env.VAPID_PUBLIC_KEY!,
      process.env.VAPID_PRIVATE_KEY!
    );
  }

  async saveSubscription(userId: string, subscription: PushSubscription): Promise<void> {
    const storage = this.sdk.storage();
    await storage.upload(
      new File(
        [JSON.stringify(subscription)],
        `push_subscription_${userId}_${btoa(subscription.endpoint)}`,
        {
          type: 'application/json'
        }
      )
    );
  }

  async removeSubscription(userId: string, endpoint: string): Promise<void> {
    const storage = this.sdk.storage();
    await storage.delete(`push_subscription_${userId}_${btoa(endpoint)}`);
  }

  async getUserSubscriptions(userId: string): Promise<PushSubscription[]> {
    const storage = this.sdk.storage();
    const allSubscriptions = await storage.list();
    return allSubscriptions.filter((sub) => sub.userId === userId);
  }

  async sendPushNotification(userId: string, notification: any): Promise<void> {
    const subscriptions = await this.getUserSubscriptions(userId);

    const payload = JSON.stringify({
      title: notification.title,
      body: notification.message,
      icon: '/icon-192x192.png',
      badge: '/badge-72x72.png',
      data: {
        url: notification.actionUrl || '/',
        type: notification.type
      }
    });

    const sendPromises = subscriptions.map(async (subscription) => {
      try {
        await webpush.sendNotification(subscription, payload);
        console.log(`Push notification sent to ${subscription.endpoint}`);
      } catch (error) {
        console.error('Push notification failed:', error);

        // Remove invalid subscriptions
        if (error.statusCode === 410) {
          await this.removeSubscription(userId, subscription.endpoint);
        }
      }
    });

    await Promise.all(sendPromises);
  }
}

Notification Analytics

Analytics Service

// src/lib/notifications/analytics.ts
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';

interface NotificationMetrics {
  notificationId: string;
  userId: string;
  type: string;
  channel: 'email' | 'push' | 'inApp' | 'websocket';
  status: 'sent' | 'delivered' | 'opened' | 'clicked' | 'failed';
  timestamp: Date;
  metadata?: any;
}

export class NotificationAnalyticsService {
  constructor(private sdk: AsterismsBackendSDK) {}

  async trackNotification(metrics: NotificationMetrics): Promise<void> {
    const storage = this.sdk.storage();
    await storage.upload(
      new File(
        [
          JSON.stringify({
            ...metrics,
            id: crypto.randomUUID()
          })
        ],
        `notification_metric_${Date.now()}`,
        {
          type: 'application/json'
        }
      )
    );
  }

  async getNotificationStats(userId?: string, startDate?: Date, endDate?: Date): Promise<any> {
    const storage = this.sdk.storage();
    let metrics = await storage.list('notification_metrics');

    // Filter by user if specified
    if (userId) {
      metrics = metrics.filter((m) => m.userId === userId);
    }

    // Filter by date range if specified
    if (startDate || endDate) {
      metrics = metrics.filter((m) => {
        const metricDate = new Date(m.timestamp);
        return (!startDate || metricDate >= startDate) && (!endDate || metricDate <= endDate);
      });
    }

    // Calculate statistics
    const stats = {
      total: metrics.length,
      byChannel: {},
      byType: {},
      byStatus: {},
      deliveryRate: 0,
      openRate: 0,
      clickRate: 0
    };

    metrics.forEach((metric) => {
      // Count by channel
      stats.byChannel[metric.channel] = (stats.byChannel[metric.channel] || 0) + 1;

      // Count by type
      stats.byType[metric.type] = (stats.byType[metric.type] || 0) + 1;

      // Count by status
      stats.byStatus[metric.status] = (stats.byStatus[metric.status] || 0) + 1;
    });

    // Calculate rates
    const sentCount = stats.byStatus['sent'] || 0;
    const deliveredCount = stats.byStatus['delivered'] || 0;
    const openedCount = stats.byStatus['opened'] || 0;
    const clickedCount = stats.byStatus['clicked'] || 0;

    if (sentCount > 0) {
      stats.deliveryRate = (deliveredCount / sentCount) * 100;
      stats.openRate = (openedCount / sentCount) * 100;
      stats.clickRate = (clickedCount / sentCount) * 100;
    }

    return stats;
  }
}

Testing Notifications

Unit Tests

// src/lib/notifications/templates.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { NotificationTemplates } from './templates';

// Mock SDK for testing
function createMockSDK() {
  return {
    notifications: () => ({
      sendToAccount: vi.fn().mockResolvedValue({ success: true }),
      sendToRole: vi.fn().mockResolvedValue({ success: true }),
      sendToAll: vi.fn().mockResolvedValue({ success: true }),
      sendToChannel: vi.fn().mockResolvedValue({ success: true })
    })
  };
}

describe('NotificationTemplates', () => {
  let templates: NotificationTemplates;
  let mockSDK: ReturnType<typeof createMockSDK>;

  beforeEach(() => {
    mockSDK = createMockSDK();
    templates = new NotificationTemplates(mockSDK);
  });

  describe('sendUserRegistrationNotification', () => {
    it('should send welcome notification to user', async () => {
      const accountId = 'user123';
      const userName = 'John Doe';

      await templates.sendUserRegistrationNotification(accountId, userName);

      expect(mockSDK.notifications().sendToAccount).toHaveBeenCalledWith(
        accountId,
        expect.objectContaining({
          sender: 'your-app',
          message: expect.objectContaining({
            subject: `Welcome to Our Platform, ${userName}!`,
            text: expect.stringContaining('Your account has been created')
          })
        })
      );
    });
  });

  describe('sendPasswordResetNotification', () => {
    it('should send password reset notification', async () => {
      const accountId = 'user123';
      const resetToken = 'reset-token-123';

      await templates.sendPasswordResetNotification(accountId, resetToken);

      expect(mockSDK.notifications().sendToAccount).toHaveBeenCalledWith(
        accountId,
        expect.objectContaining({
          sender: 'your-app',
          message: expect.objectContaining({
            subject: 'Password Reset Request',
            urgency: 'IMPORTANT'
          })
        })
      );
    });
  });
});

Integration Tests

// src/routes/api/notifications/+server.test.ts
import { describe, it, expect } from 'vitest';
import { POST } from './+server';

// Mock SDK for testing
function createMockSDK() {
  return {
    notifications: () => ({
      sendToAccount: vi.fn().mockResolvedValue({ success: true }),
      sendToRole: vi.fn().mockResolvedValue({ success: true }),
      sendToAll: vi.fn().mockResolvedValue({ success: true }),
      sendToChannel: vi.fn().mockResolvedValue({ success: true })
    })
  };
}

describe('Notification API', () => {
  it('should send notification to user', async () => {
    const mockSDK = createMockSDK();

    const mockEvent = {
      request: {
        json: async () => ({
          userId: 'user123',
          notification: {
            subject: 'Test Notification',
            text: 'This is a test message',
            urgency: 'NONE'
          }
        })
      },
      locals: { sdk: mockSDK }
    };

    const response = await POST(mockEvent as any);
    const data = await response.json();

    expect(response.status).toBe(200);
    expect(data.success).toBe(true);
    expect(mockSDK.notifications().sendToAccount).toHaveBeenCalledWith(
      'user123',
      expect.objectContaining({
        sender: 'your-app',
        message: expect.objectContaining({
          subject: 'Test Notification',
          text: 'This is a test message',
          urgency: 'NONE'
        })
      })
    );
  });
  it('should send notification to workspace', async () => {
    const mockSDK = createMockSDK();

    const mockEvent = {
      request: {
        json: async () => ({
          workspaceId: 'workspace123',
          notification: {
            subject: 'Workspace Notification',
            text: 'This is a workspace message',
            urgency: 'IMPORTANT'
          }
        })
      },
      locals: { sdk: mockSDK }
    };

    const response = await POST(mockEvent as any);
    const data = await response.json();

    expect(response.status).toBe(200);
    expect(data.success).toBe(true);
    expect(mockSDK.notifications().sendToWorkspace).toHaveBeenCalledWith(
      'workspace123',
      expect.objectContaining({
        sender: 'your-app',
        message: expect.objectContaining({
          subject: 'Workspace Notification',
          text: 'This is a workspace message',
          urgency: 'IMPORTANT'
        })
      })
    );
  });
});

Production Considerations

Rate Limiting

// src/lib/notifications/rateLimiter.ts
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';

export class NotificationRateLimiter {
  constructor(private sdk: AsterismsBackendSDK) {}

  async canSendNotification(userId: string, type: string): Promise<boolean> {
    const storage = this.sdk.storage();
    const now = Date.now();
    const windowMs = 60 * 60 * 1000; // 1 hour
    const limits = {
      security: 5,
      marketing: 10,
      system: 20,
      default: 50
    };

    const limit = limits[type] || limits['default'];
    const key = `rate_limit:${userId}:${type}`;

    const requests = (await storage.get(key)) || [];
    const recentRequests = requests.filter((time) => now - time < windowMs);

    if (recentRequests.length >= limit) {
      return false;
    }

    // Add current request
    recentRequests.push(now);
    await storage.set(key, recentRequests);

    return true;
  }
}

Error Handling and Retries

// src/lib/notifications/errorHandler.ts
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';

export class NotificationErrorHandler {
  constructor(private sdk: AsterismsBackendSDK) {}

  async handleFailedNotification(
    notificationId: string,
    error: Error,
    attempt: number
  ): Promise<void> {
    const storage = this.sdk.storage();
    const maxRetries = 3;

    if (attempt < maxRetries) {
      // Schedule retry
      const retryDelay = Math.pow(2, attempt) * 1000; // Exponential backoff
      const retryAt = new Date(Date.now() + retryDelay);

      await storage.update('notification_queue', notificationId, {
        status: 'pending',
        scheduledFor: retryAt,
        attempts: attempt + 1,
        lastError: error.message
      });
    } else {
      // Mark as permanently failed
      await storage.update('notification_queue', notificationId, {
        status: 'failed',
        lastError: error.message
      });

      // Log to monitoring system
      console.error(`Notification ${notificationId} failed permanently:`, error);
    }
  }
}

NotificationDelivery Type Reference

The NotificationDelivery type structure mirrors the Java microservice implementation:

interface NotificationDelivery {
  schedule: 'IMMEDIATE' | 'WITH_DELAY';
  delay: string; // Java Duration format - ISO-8601 duration format
}

Important Notes:

  • The delay field uses Java Duration serialization format (ISO-8601 duration format)
  • This corresponds to the Java Duration type in the notification microservice
  • Format: PnDTnHnMn.nS where P is duration designator, T separates date/time components
  • For immediate delivery: { schedule: 'IMMEDIATE', delay: 'PT0S' }
  • For delayed delivery: { schedule: 'WITH_DELAY', delay: 'PT30M' } (30 minutes)

Examples:

// Send immediately
{ schedule: 'IMMEDIATE', delay: 'PT0S' }

// Send after 30 minutes
{ schedule: 'WITH_DELAY', delay: 'PT30M' }

// Send after 1 hour and 30 minutes
{ schedule: 'WITH_DELAY', delay: 'PT1H30M' }

// Send after 2 days
{ schedule: 'WITH_DELAY', delay: 'P2D' }

// Send after 8 hours, 6 minutes, and 12.345 seconds
{ schedule: 'WITH_DELAY', delay: 'PT8H6M12.345S' }

Next Steps