Skip to Content
ServicesSMS Service

SMS Service

Reliable SMS messaging service using Twilio. Send OTP codes, transactional messages, and notifications via SMS.


Installation & Setup

1. Install Twilio SDK

pnpm add twilio

2. Configure Environment Variables

# .env TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxx TWILIO_AUTH_TOKEN=your_auth_token TWILIO_PHONE_NUMBER=+1234567890

3. Usage

The SMS Service is available as a singleton via getSMSService.

import { getSMSService } from '@orbitusdev/core/services/sms'; const smsService = getSMSService();

Supported Providers

  • Twilio (Default)
  • AWS SNS (Coming soon)

OTP (One-Time Password) Messages

Generate and Send OTP

// lib/otp.ts import { getSMSService } from '@orbitusdev/core/services/sms'; import { randomInt } from 'crypto'; export function generateOTP(length = 6): string { const min = Math.pow(10, length - 1); const max = Math.pow(10, length) - 1; return randomInt(min, max).toString(); } export async function sendOTP(phoneNumber: string, locale: 'en' | 'tr' = 'en') { const smsService = getSMSService(); const otp = generateOTP(); // Store OTP in database with expiration await prisma.otp.create({ data: { phoneNumber, code: otp, expiresAt: new Date(Date.now() + 5 * 60 * 1000) // 5 minutes } }); const messages = { en: `Your verification code is: ${otp}. Valid for 5 minutes.`, tr: `Doğrulama kodunuz: ${otp}. 5 dakika geçerlidir.` }; await smsService.sendSMS({ to: phoneNumber, message: messages[locale] }); return { success: true }; }

Verify OTP

export async function verifyOTP(phoneNumber: string, code: string) { const otp = await prisma.otp.findFirst({ where: { phoneNumber, code, expiresAt: { gt: new Date() }, verified: false } }); if (!otp) { return { success: false, error: 'Invalid or expired OTP' }; } // Mark as verified await prisma.otp.update({ where: { id: otp.id }, data: { verified: true } }); return { success: true }; }

Transactional Messages

Order Confirmation

import { getSMSService } from '@orbitusdev/core/services/sms'; export async function sendOrderConfirmation(order: { id: string; phoneNumber: string; total: number; locale: 'en' | 'tr'; }) { const smsService = getSMSService(); const messages = { en: `Order #${order.id} confirmed! Total: $${order.total}. Track your order at orbitus.dev/orders/${order.id}`, tr: `Sipariş #${order.id} onaylandı! Toplam: ₺${order.total}. Siparişinizi takip edin: orbitus.dev/orders/${order.id}` }; await smsService.sendSMS({ to: order.phoneNumber, message: messages[order.locale] }); }

Delivery Updates

import { getSMSService } from '@orbitusdev/core/services/sms'; export async function sendDeliveryUpdate(delivery: { orderId: string; phoneNumber: string; status: 'shipped' | 'out_for_delivery' | 'delivered'; locale: 'en' | 'tr'; }) { const smsService = getSMSService(); const statusMessages = { en: { shipped: `Your order #${delivery.orderId} has been shipped!`, out_for_delivery: `Your order #${delivery.orderId} is out for delivery.`, delivered: `Your order #${delivery.orderId} has been delivered. Enjoy!` }, tr: { shipped: `Siparişiniz #${delivery.orderId} kargoya verildi!`, out_for_delivery: `Siparişiniz #${delivery.orderId} dağıtıma çıktı.`, delivered: `Siparişiniz #${delivery.orderId} teslim edildi. Afiyet olsun!` } }; await smsService.sendSMS({ to: delivery.phoneNumber, message: statusMessages[delivery.locale][delivery.status] }); }

Payment Alerts

import { getSMSService } from '@orbitusdev/core/services/sms'; export async function sendPaymentAlert(payment: { phoneNumber: string; amount: number; status: 'success' | 'failed'; locale: 'en' | 'tr'; }) { const smsService = getSMSService(); const messages = { en: { success: `Payment of $${payment.amount} successful. Thank you!`, failed: `Payment of $${payment.amount} failed. Please try again.` }, tr: { success: `₺${payment.amount} tutarındaki ödeme başarılı. Teşekkürler!`, failed: `₺${payment.amount} tutarındaki ödeme başarısız. Lütfen tekrar deneyin.` } }; await smsService.sendSMS({ to: payment.phoneNumber, message: messages[payment.locale][payment.status] }); }

Server Actions

Send OTP Action

'use server'; import { sendOTP } from '@/lib/otp'; import { z } from 'zod'; const phoneSchema = z.string().regex(/^\+[1-9]\d{1,14}$/); export async function sendOTPAction(phoneNumber: string, locale: 'en' | 'tr') { // Validate phone number (E.164 format) const result = phoneSchema.safeParse(phoneNumber); if (!result.success) { return { success: false, error: 'Invalid phone number format' }; } try { await sendOTP(phoneNumber, locale); return { success: true }; } catch (error) { return { success: false, error: 'Failed to send OTP' }; } }

Verify OTP Action

'use server'; import { verifyOTP } from '@/lib/otp'; export async function verifyOTPAction(phoneNumber: string, code: string) { try { const result = await verifyOTP(phoneNumber, code); return result; } catch (error) { return { success: false, error: 'Verification failed' }; } }

Client Components

OTP Input Form

'use client'; import { useState, useTransition } from 'react'; import { sendOTPAction, verifyOTPAction } from './actions'; export function OTPForm() { const [phoneNumber, setPhoneNumber] = useState(''); const [otp, setOtp] = useState(''); const [step, setStep] = useState<'phone' | 'verify'>('phone'); const [isPending, startTransition] = useTransition(); const handleSendOTP = () => { startTransition(async () => { const result = await sendOTPAction(phoneNumber, 'en'); if (result.success) { setStep('verify'); } }); }; const handleVerifyOTP = () => { startTransition(async () => { const result = await verifyOTPAction(phoneNumber, otp); if (result.success) { // Redirect or show success } }); }; if (step === 'phone') { return ( <div> <input type="tel" value={phoneNumber} onChange={(e) => setPhoneNumber(e.target.value)} placeholder="+1234567890" /> <button onClick={handleSendOTP} disabled={isPending}> {isPending ? 'Sending...' : 'Send OTP'} </button> </div> ); } return ( <div> <input type="text" value={otp} onChange={(e) => setOtp(e.target.value)} placeholder="Enter 6-digit code" maxLength={6} /> <button onClick={handleVerifyOTP} disabled={isPending}> {isPending ? 'Verifying...' : 'Verify'} </button> </div> ); }

Bulk SMS

Send to Multiple Recipients

import { getSMSService } from '@orbitusdev/core/services/sms'; export async function sendBulkSMS(recipients: string[], message: string) { const smsService = getSMSService(); const results = await Promise.allSettled( recipients.map((phoneNumber) => smsService.sendSMS({ to: phoneNumber, message }) ) ); const successful = results.filter((r) => r.status === 'fulfilled').length; const failed = results.filter((r) => r.status === 'rejected').length; return { total: recipients.length, successful, failed }; }

Rate Limiting

Prevent SMS spam with rate limiting:

import { rateLimit } from '@orbitusdev/core/lib/security/rate-limit'; export async function sendOTPWithRateLimit(phoneNumber: string) { const { success } = await rateLimit.check(`sms:${phoneNumber}`, { limit: 3, // 3 SMS window: 3600000 // per hour }); if (!success) { throw new Error('Too many SMS requests. Please try again later.'); } return sendOTP(phoneNumber); }

Error Handling

import { getSMSService } from '@orbitusdev/core/services/sms'; import { logger } from '@orbitusdev/core/services/logger'; export async function sendSMSSafe(params: SendSMSParams) { const smsService = getSMSService(); try { const result = await smsService.sendSMS(params); logger.info('SMS sent successfully', { to: params.to }); return result; } catch (error) { logger.error('Failed to send SMS', error, { to: params.to }); // Retry logic if (error.code === 'NETWORK_ERROR') { await delay(1000); return smsService.sendSMS(params); } throw error; } }

Best Practices

  1. Validate phone numbers - Use E.164 format (+1234567890)
  2. Rate limit - Prevent spam and abuse
  3. Short messages - SMS has 160 character limit
  4. Include brand name - Identify yourself in messages
  5. Provide opt-out - Include “Reply STOP to unsubscribe”
  6. Respect time zones - Don’t send at night
  7. Store OTPs securely - Hash in database
  8. Set expiration - OTPs should expire quickly (5-10 min)
  9. Log all SMS - For debugging and auditing
  10. Handle errors - Gracefully fail, provide alternatives

Security Considerations

  1. Never expose Twilio credentials - Use environment variables
  2. Validate all inputs - Prevent injection attacks
  3. Rate limit strictly - SMS is expensive and can be abused
  4. Use HTTPS only - For webhook callbacks
  5. Verify webhook signatures - Ensure requests are from Twilio
  6. Expire OTPs quickly - 5 minutes maximum
  7. Limit OTP attempts - 3-5 attempts before lockout
  8. Log security events - Track suspicious activity

Cost Optimization

// Use a pool of phone numbers to avoid rate limits const phoneNumbers = [ process.env.TWILIO_PHONE_1, process.env.TWILIO_PHONE_2, process.env.TWILIO_PHONE_3 ]; function getPhoneNumber(recipientNumber: string): string { // Round-robin or hash-based selection const index = Math.abs(hashCode(recipientNumber)) % phoneNumbers.length; return phoneNumbers[index]!; } function hashCode(str: string): number { let hash = 0; for (let i = 0; i < str.length; i++) { hash = (hash << 5) - hash + str.charCodeAt(i); hash |= 0; } return hash; }

Testing

Mock SMS Service

// tests/mocks/sms.ts export const mockSendSMS = jest.fn().mockResolvedValue({ success: true, messageId: 'SM1234567890', status: 'sent' }); jest.mock('@orbitusdev/core/services/sms', () => ({ sendSMS: mockSendSMS }));

Test with Twilio Test Credentials

Twilio provides test credentials that don’t send real SMS:

TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx TWILIO_AUTH_TOKEN=your_test_auth_token

Last updated on