| name | stacks-sms |
| description | Use when implementing SMS in Stacks — sending text messages, the SmsBuilder fluent API, SMS templates, phone verification (OTP/2FA), bulk sending, Twilio/Vonage drivers, E.164 formatting, or the SMS facade. Covers @stacksjs/sms and config/sms.ts. |
| license | MIT |
| compatibility | Bun >= 1.3.0, TypeScript |
| allowed-tools | Read Edit Write Bash Grep Glob |
Stacks SMS
Multi-driver SMS system with verification (OTP), templates, bulk sending, and a fluent builder API. Two fully implemented drivers: Twilio and Vonage.
Key Paths
- Core package:
storage/framework/core/sms/src/
- SMS facade and builder:
storage/framework/core/sms/src/sms.ts
- Twilio driver:
storage/framework/core/sms/src/drivers/twilio.ts
- Vonage driver:
storage/framework/core/sms/src/drivers/vonage.ts
- Drivers index:
storage/framework/core/sms/src/drivers/index.ts
- Configuration:
config/sms.ts
Package Exports
import SMS, { sms, send, sendSms, sendBulk, sendTemplate } from '@stacksjs/sms'
import { startVerification, checkVerification, cancelVerification } from '@stacksjs/sms'
import { formatE164, isValidPhoneNumber, isEnabled, getConfig, configure, getDriver, init } from '@stacksjs/sms'
import { SmsBuilder } from '@stacksjs/sms'
import { TwilioDriver, createTwilioDriver } from '@stacksjs/sms'
import { VonageDriver, createVonageDriver } from '@stacksjs/sms'
SMS Facade Object
The SMS default export aggregates all functions:
SMS.init()
SMS.configure(config)
SMS.isEnabled()
SMS.getConfig()
SMS.send(message)
SMS.sendSms(message)
SMS.sendBulk(messages)
SMS.sendTemplate(to, name, vars)
SMS.getStatus(messageId)
SMS.getBalance()
SMS.verifyNumber(phone)
SMS.startVerification(req)
SMS.checkVerification(req)
SMS.cancelVerification(id)
SMS.formatE164(phone, cc?)
.(phone)
.()
.(provider?)
.(p?)
SmsBuilder (Fluent API)
import { sms } from '@stacksjs/sms'
await sms()
.to('+1234567890')
.body('Your code is 123456')
.from('+0987654321')
.send()
await sms()
.to('+1234567890')
.body('Hello!')
.via('vonage')
.send()
await sms()
.to(['+1111111111', '+2222222222'])
.body('Check this out')
.media(['https://example.com/image.jpg'])
.callback('https://myapp.com/sms/status')
.send()
await sms()
.to('+1234567890')
.text('Hello!')
.send()
The builder validates that to and body are set before sending. If missing, it returns a failed SmsSendResult without throwing. If .via(provider) is set, it creates a new driver for that provider; otherwise uses the default driver.
Direct Send Functions
import { send, sendBulk } from '@stacksjs/sms'
await send({
to: '+1234567890',
body: 'Hello!',
from: '+0987654321',
mediaUrls: ['url'],
statusCallback: 'url',
})
await sendBulk([msg1, msg2, msg3])
send() and all sending functions call ensureConfig() first, which lazily loads config/sms.ts via dynamic import.
Message Status & Info
import { getStatus, verifyNumber, getBalance } from '@stacksjs/sms'
const status = await getStatus('SM_message_id')
const info = await verifyNumber('+1234567890')
const balance = await getBalance()
Phone Verification (OTP/2FA)
import { startVerification, checkVerification, cancelVerification } from '@stacksjs/sms'
const result = await startVerification({
to: '+1234567890',
channel: 'sms',
codeLength: 6,
locale: 'en',
customMessage: 'Your code: {code}',
})
const check = await checkVerification({
to: '+1234567890',
verificationId: result.verificationId,
code: '123456',
})
const cancelled = await cancelVerification(result.verificationId!)
Twilio uses the Verify service (verify.twilio.com/v2) and requires verifyServiceSid passed to the TwilioDriver constructor.
Vonage uses the Verify V2 API (api.nexmo.com/v2/verify). Whatsapp channel maps to whatsapp_interactive.
SMS Templates
import { sendTemplate } from '@stacksjs/sms'
await sendTemplate('+1234567890', 'order-confirmation', {
orderNumber: '#12345',
total: '$29.99',
})
Templates are defined in config/sms.ts under templates[]. Each template has name, body, and optional variables. Variables use single-brace syntax {variableName} in the template body. The function finds the template by name, replaces variables, and sends via the default driver.
Returns a failed result (without throwing) if the template is not found.
Phone Number Utilities
import { formatE164, isValidPhoneNumber } from '@stacksjs/sms'
formatE164('+1 (234) 567-890')
formatE164('2345678900', '1')
formatE164('002345678900')
isValidPhoneNumber('+1234567890')
isValidPhoneNumber('1234567890')
formatE164() strips spaces, dashes, and parentheses. If the number starts with +, returns as-is. If it starts with 00, replaces with +. Otherwise prepends + and the default country code (from config.defaultCountryCode or '1' for US).
Twilio Driver
import { TwilioDriver, createTwilioDriver } from '@stacksjs/sms'
const driver = new TwilioDriver({
accountSid: 'AC...',
authToken: '...',
from: '+1234567890',
messagingServiceSid: 'MG...',
statusCallback: 'https://...',
}, 'VA_verify_service_sid')
const driver = createTwilioDriver(config, verifyServiceSid?)
Uses REST API at https://api.twilio.com/2010-04-01. Auth via HTTP Basic (accountSid:authToken).
Features:
send(message) -- sends via Messages API, supports MMS (mediaUrls), statusCallback, messagingServiceSid
sendBulk(messages) -- sends in parallel via Promise.all
getStatus(messageId) -- retrieves message status
verify(phoneNumber) -- uses Twilio Lookup API v2 with line_type_intelligence
getBalance() -- retrieves account balance
startVerification(req) -- Twilio Verify service
checkVerification(req) -- check OTP code
cancelVerification(id) -- cancel pending verification
Status mapping: queued, sending, sent, delivered, undelivered -> SmsStatus; canceled -> 'failed'.
Vonage Driver
import { VonageDriver, createVonageDriver } from '@stacksjs/sms'
const driver = new VonageDriver({
apiKey: '...',
apiSecret: '...',
from: 'MyApp',
applicationId: '...',
privateKey: '...',
}, true)
const driver = createVonageDriver(config, useMessagesApi?)
Two send modes:
- SMS API (legacy, default) --
https://rest.nexmo.com/sms/json, uses API key/secret in body
- Messages API (newer) --
https://api.nexmo.com/v1/messages, uses Basic auth or JWT
Features:
send(message) -- sends via chosen API mode
sendBulk(messages) -- sends in parallel
getStatus(messageId) -- Messages API only
verify(phoneNumber) -- Vonage Number Insight API (basic)
getBalance() -- account balance in EUR
startVerification(req) -- Vonage Verify V2 API
checkVerification(req) -- requires verificationId
cancelVerification(id) -- DELETE request to cancel
The nexmo export is a legacy alias for vonage.
Other Drivers (Commented Out / Placeholder)
The drivers/ directory contains placeholder files for: gupshup, nexmo (alias for vonage), plivo, sms77, sns, telnyx, termii. These are all commented out and not functional. Only Twilio and Vonage are active.
config/sms.ts
{
enabled: false,
provider: 'twilio',
from: env.SMS_FROM_NUMBER || '',
defaultCountryCode: 'US',
messageType: 'TRANSACTIONAL',
drivers: {
twilio: {
accountSid: env.TWILIO_ACCOUNT_SID || '',
authToken: env.TWILIO_AUTH_TOKEN || '',
from: env.TWILIO_FROM_NUMBER || '',
messagingServiceSid: env.TWILIO_MESSAGING_SERVICE_SID || '',
},
vonage: {
apiKey: env.VONAGE_API_KEY || '',
apiSecret: env.VONAGE_API_SECRET || '',
from: env.VONAGE_FROM_NUMBER || '',
},
pinpoint: {
region: env.AWS_REGION || 'us-east-1',
accessKeyId: env.AWS_ACCESS_KEY_ID || '',
secretAccessKey: env.AWS_SECRET_ACCESS_KEY || '',
senderId: env.SMS_SENDER_ID || ,
: env. || ,
},
},
: ,
: {
: ,
: [, , , , ],
},
: [],
: {
: ,
},
}
Type Interfaces (from @stacksjs/types)
interface SmsMessage {
to: string | string[]
body: string
from?: string
mediaUrls?: string[]
statusCallback?: string
}
interface SmsSendResult {
success: boolean
messageId?: string
status?: SmsStatus
to: string
provider: string
error?: string
segments?: number
price?: number
currency?: string
}
type SmsStatus = 'queued' | 'sending' | 'sent' | 'delivered' | 'undelivered' | 'failed' | 'unknown'
interface SmsStatusUpdate {
messageId: string
to: string
status: SmsStatus
timestamp: Date
errorCode?: string
errorMessage?:
}
= | |
{
:
?:
?:
?:
?:
}
{
:
:
?:
}
{
:
?:
: | |
?:
}
Gotchas
- SMS is disabled by default (
enabled: false) -- must set to true in config/sms.ts
- Provider API keys go in
.env, not config files
- Config is loaded lazily via dynamic import on first
send()/operation -- init() pre-loads it
- Only Twilio and Vonage drivers are fully implemented; other drivers (gupshup, plivo, sns, telnyx, etc.) are commented-out placeholders
pinpoint appears in config but has no driver implementation in getDriver() -- using it throws 'Unsupported SMS provider: pinpoint'
- The
defaultCountryCode in config is 'US' but formatE164() uses it as a numeric code prefix (the fallback is '1'), so the actual behavior uses the number '1' for US
- Template variables use single-brace syntax
{variableName}, NOT double-brace
sendBulk() sends all messages in parallel via Promise.all -- no rate limiting
- Twilio verification requires a
verifyServiceSid passed to the driver constructor, which is not part of the standard config structure
- Vonage JWT authentication is a simplified placeholder -- real RS256 signing is not fully implemented
getDriver() creates a new driver instance each time unless accessed through the cached getDefaultDriver()
sendTemplate() returns a failed result (not an exception) if the template name is not found