Skip to Content
Packages@loop/notifications

@loop/notifications

Email notification system with React Email templates, Resend (transactional), and Klaviyo (marketing) clients.

Installation

pnpm add @loop/notifications

Overview

Unified notification system for subscription lifecycle emails, order confirmations, and marketing automation.

Key features:

  • React Email templates (type-safe, component-based)
  • Resend client (transactional emails)
  • Klaviyo client (marketing lists + events)
  • Unified send() interface

Quick Start

import { sendEmail } from '@loop/notifications' await sendEmail({ to: 'customer@example.com', template: 'subscription-cancelled', data: { customerName: 'John Doe', subscriptionId: 'sub_123', cancelledAt: new Date() } })

Email Templates

All templates built with React Email :

Subscription Lifecycle

import { sendSubscriptionCancelled } from '@loop/notifications/templates' await sendSubscriptionCancelled({ to: 'customer@example.com', data: { customerName: 'John Doe', subscriptionId: 'sub_123', tier: 'Plus', cancelledAt: new Date(), refundAmount: 4900 // $49.00 in cents } })

Available templates:

  • subscription-cancelled - Cancellation confirmation + next steps
  • subscription-resumed - Welcome back message
  • subscription-paused - Pause confirmation
  • payment-failed - Payment failure with retry info
  • payment-recovered - Successful retry notification

Payment Templates

import { sendPaymentFailed } from '@loop/notifications/templates' await sendPaymentFailed({ to: 'customer@example.com', data: { customerName: 'John Doe', amount: 4900, declineReason: 'insufficient_funds', retryDate: new Date('2026-03-25'), updatePaymentUrl: 'https://my.loop.health/settings/billing' } })

Order Templates

import { sendOrderConfirmation } from '@loop/notifications/templates' await sendOrderConfirmation({ to: 'customer@example.com', data: { orderNumber: '12345', items: [ { name: 'BPC-157 5mg', quantity: 2, price: 4900 } ], total: 9800, trackingUrl: 'https://...' } })

Resend Client (Transactional)

import { ResendClient } from '@loop/notifications/clients/resend' const resend = new ResendClient({ apiKey: process.env.RESEND_API_KEY! }) // Send email await resend.send({ from: 'Loop Health <noreply@loop.health>', to: 'customer@example.com', subject: 'Your subscription has been cancelled', html: '<p>We\'re sorry to see you go...</p>' }) // Send with React Email template await resend.sendTemplate({ from: 'Loop Health <noreply@loop.health>', to: 'customer@example.com', template: 'subscription-cancelled', data: { ... } })

Klaviyo Client (Marketing)

import { KlaviyoClient } from '@loop/notifications/clients/klaviyo' const klaviyo = new KlaviyoClient({ apiKey: process.env.KLAVIYO_API_KEY! }) // Add to list await klaviyo.addToList({ email: 'customer@example.com', listId: 'abc123', properties: { firstName: 'John', lastName: 'Doe', tier: 'Plus' } }) // Track event await klaviyo.trackEvent({ email: 'customer@example.com', event: 'Subscription Cancelled', properties: { subscriptionId: 'sub_123', tier: 'Plus', revenue: -4900 } })

Klaviyo Event Tracking

Subscription events:

  • Subscription Created
  • Subscription Cancelled
  • Subscription Resumed
  • Subscription Paused

Payment events:

  • Payment Succeeded
  • Payment Failed
  • Payment Recovered

Order events:

  • Order Placed
  • Order Shipped
  • Order Delivered

Unified Send Interface

import { sendEmail } from '@loop/notifications' // Automatically routes to correct provider await sendEmail({ to: 'customer@example.com', template: 'subscription-cancelled', data: { ... }, provider: 'resend' // or 'klaviyo' })

Template Development

Creating New Templates

  1. Create template file:
// templates/subscription-welcome.tsx import { Html, Head, Body, Container, Text, Button } from '@react-email/components' interface WelcomeEmailProps { customerName: string tier: string dashboardUrl: string } export default function WelcomeEmail({ customerName, tier, dashboardUrl }: WelcomeEmailProps) { return ( <Html> <Head /> <Body> <Container> <Text>Hi {customerName},</Text> <Text>Welcome to Loop {tier}!</Text> <Button href={dashboardUrl}>Go to Dashboard</Button> </Container> </Body> </Html> ) }
  1. Add to template registry:
// templates/index.ts export { default as subscriptionWelcome } from './subscription-welcome'
  1. Use it:
import { sendEmail } from '@loop/notifications' await sendEmail({ template: 'subscription-welcome', data: { customerName: 'John', tier: 'Plus', dashboardUrl: 'https://my.loop.health' } })

Preview Templates

# Start React Email dev server pnpm --filter @loop/notifications dev:email # Open http://localhost:3000 to preview all templates

Environment Variables

# Resend (transactional) RESEND_API_KEY=re_xxx # Klaviyo (marketing) KLAVIYO_API_KEY=pk_xxx # Email defaults EMAIL_FROM_NAME="Loop Health" EMAIL_FROM_ADDRESS="noreply@loop.health" EMAIL_REPLY_TO="support@loop.health"

Error Handling

All send methods return Result<T, AppError>:

const result = await sendEmail({ ... }) if (result.ok) { console.log('Email sent:', result.data.messageId) } else { console.error('Send failed:', result.error.message) }

Testing

# Run tests pnpm --filter @loop/notifications test # Send test email pnpm --filter @loop/notifications test:send

Production Checklist

  • Configure RESEND_API_KEY
  • Configure KLAVIYO_API_KEY
  • Verify sender domain (loop.health)
  • Set up SPF/DKIM/DMARC records
  • Test all templates in production
  • Monitor bounce rates
  • Set up unsubscribe handling

Architecture

React Email benefits:

  • Type-safe templates
  • Component reusability
  • Hot reload during development
  • Preview server for QA
  • Export to plain HTML

Provider abstraction:

  • Swap providers without changing code
  • Easy to add SendGrid, Postmark, etc.
  • Unified interface across channels

Migration from loopbio-v2

// Old (loopbio-v2) import { sendEmail } from '@/lib/integrations/resend' // New (@loop/notifications) import { sendEmail } from '@loop/notifications'

Source

  • PR: #242 
  • Lines: 2,495
  • Files: 23
  • Shipped: 2026-03-21

Examples

Complete Subscription Cancellation Flow

import { sendSubscriptionCancelled } from '@loop/notifications/templates' import { KlaviyoClient } from '@loop/notifications/clients/klaviyo' async function handleCancellation(subscription: Subscription) { // 1. Send transactional email (Resend) await sendSubscriptionCancelled({ to: subscription.customer.email, data: { customerName: subscription.customer.name, subscriptionId: subscription.id, tier: subscription.tier, cancelledAt: new Date() } }) // 2. Track event in Klaviyo const klaviyo = new KlaviyoClient({ ... }) await klaviyo.trackEvent({ email: subscription.customer.email, event: 'Subscription Cancelled', properties: { subscriptionId: subscription.id, tier: subscription.tier, revenue: -subscription.price } }) // 3. Remove from active subscriber list await klaviyo.removeFromList({ email: subscription.customer.email, listId: 'active-subscribers' }) }