@loop/notifications
Email notification system with React Email templates, Resend (transactional), and Klaviyo (marketing) clients.
Installation
pnpm add @loop/notificationsOverview
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 stepssubscription-resumed- Welcome back messagesubscription-paused- Pause confirmationpayment-failed- Payment failure with retry infopayment-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 CreatedSubscription CancelledSubscription ResumedSubscription Paused
Payment events:
Payment SucceededPayment FailedPayment Recovered
Order events:
Order PlacedOrder ShippedOrder 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
- 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>
)
}- Add to template registry:
// templates/index.ts
export { default as subscriptionWelcome } from './subscription-welcome'- 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 templatesEnvironment 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:sendProduction 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'Related Packages
- @loop/commerce - Subscription lifecycle events
- @loop/core - Result monad for error handling
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'
})
}