TextPilot

Building Notification Infrastructure with TextPilot and Resend

How to build a multi-channel notification system using TextPilot for SMS and Resend for email — the modern developer's approach to messaging.

Building Notification Infrastructure with TextPilot and Resend

Every app eventually needs to notify users. Order confirmations, security alerts, appointment reminders, marketing updates — the list grows fast. The question isn't whether you need notifications, but how you build them without drowning in complexity.

Here's our take: use best-in-class tools for each channel and compose them together.

The Stack

  • Resend for email — beautiful API, React Email templates, great deliverability
  • TextPilot for SMS — three lines of code, TypeScript-first, credit-based billing
  • Your app for orchestration — you decide who gets what, when

Setting Up

npm install textpilot resend
import { TextPilot } from 'textpilot'
import { Resend } from 'resend'

const sms = new TextPilot(process.env.TEXTPILOT_API_KEY!)
const email = new Resend(process.env.RESEND_API_KEY!)

Both SDKs are TypeScript-first, zero-dependency, and work in any runtime — Node.js, Cloudflare Workers, Vercel Edge, Deno, Bun. The pattern is the same: import, init, send.

Building a Notification Service

Here's a simple notification service that routes messages to the right channel:

type Channel = 'sms' | 'email' | 'both'

interface NotifyParams {
  userId: string
  channel: Channel
  subject: string
  message: string
}

async function notify({ userId, channel, subject, message }: NotifyParams) {
  const user = await db.getUser(userId)
  const results = []

  if (channel === 'email' || channel === 'both') {
    const emailResult = await email.emails.send({
      from: 'TextPilot <notifications@textpilot.dev>',
      to: user.email,
      subject,
      text: message,
    })
    results.push({ channel: 'email', ...emailResult })
  }

  if (channel === 'sms' || channel === 'both') {
    const smsResult = await sms.send(user.phone, message)
    results.push({ channel: 'sms', ...smsResult })
  }

  return results
}

Real Examples

Order Confirmation

await notify({
  userId: order.userId,
  channel: 'both',
  subject: `Order #${order.id} confirmed`,
  message: `Your order #${order.id} is confirmed! Total: $${order.total}. We'll text you when it ships.`,
})

Email for the detailed receipt. SMS for the instant confirmation. Users get both.

Security Alert

// Security alerts should always hit SMS — highest urgency
await notify({
  userId: account.userId,
  channel: 'sms',
  subject: 'Security Alert',
  message: `New login from ${location}. If this wasn't you, secure your account: ${resetUrl}`,
})

Appointment Reminder

// Send email 24h before, SMS 1h before
await scheduleNotification({
  userId: appointment.userId,
  channel: 'email',
  sendAt: subHours(appointment.time, 24),
  subject: 'Appointment tomorrow',
  message: `Reminder: you have an appointment tomorrow at ${format(appointment.time, 'h:mm a')}.`,
})

await scheduleNotification({
  userId: appointment.userId,
  channel: 'sms',
  sendAt: subHours(appointment.time, 1),
  subject: 'Appointment in 1 hour',
  message: `Your appointment is in 1 hour at ${format(appointment.time, 'h:mm a')}. Reply CANCEL to reschedule.`,
})

User Preferences

Let users control their notification channels:

interface NotificationPreferences {
  marketing: Channel | 'none'
  transactional: Channel
  security: 'both' // Always both for security
}

async function smartNotify(
  userId: string,
  category: keyof NotificationPreferences,
  params: Omit<NotifyParams, 'userId' | 'channel'>
) {
  const prefs = await db.getNotificationPreferences(userId)
  const channel = prefs[category]

  if (channel === 'none') return []

  return notify({ userId, channel, ...params })
}

Why This Approach Works

  1. Each tool is best-in-class: Resend for email, TextPilot for SMS. No compromises.
  2. Simple composition: Both SDKs have the same pattern — import, init, send. Easy to wrap in your own abstraction.
  3. Independent scaling: Email volume spike? That's Resend's problem. SMS surge? TextPilot handles it with Cloudflare Queues.
  4. Cost efficiency: Pay for what you use on each channel separately. No bundled pricing that overcharges on one to subsidize the other.
  5. Developer experience: TypeScript all the way down. Great docs. Fast iteration.

Deployment

This runs anywhere. Here's a Cloudflare Worker example:

export default {
  async fetch(request: Request, env: Env) {
    const sms = new TextPilot(env.TEXTPILOT_API_KEY)
    const email = new Resend(env.RESEND_API_KEY)

    // Your notification logic here
  }
}

Or a Next.js API route, Express handler, Hono endpoint — the SDKs don't care about your framework.

Conclusion

Stop building notification infrastructure from scratch. Use Resend for email, TextPilot for SMS, and spend your time on the product features that actually differentiate your app.

Get started with TextPilot → | Check out Resend →