SMS
Text a notification through a pluggable SMS transport. Ships with a Twilio transport; return a plain string or a fluent SmsMessage, and route the recipient per notifiable.
The SMS channel sends a notification's text through a pluggable SmsTransport. A Twilio transport is
bundled; swap in your own for any provider. Return a plain string for the message body, or a fluent
SmsMessage when you want to override the sender number.
Install
pnpm add @dudousxd/nestjs-notifications-sms twilionpm install @dudousxd/nestjs-notifications-sms twilioThe twilio SDK is only needed for the built-in TwilioTransport. Skip it if you supply a custom
transport.
Register the channel
import { SmsChannelModule } from '@dudousxd/nestjs-notifications-sms';
@Module({
imports: [
SmsChannelModule.forRoot({
from: '+15555550100',
twilio: {
accountSid: process.env.TWILIO_ACCOUNT_SID!,
authToken: process.env.TWILIO_AUTH_TOKEN!,
},
}),
],
})
export class AppModule {}SmsChannelModule.forRoot() takes:
| Option | Type | Default | Description |
|---|---|---|---|
from | string | — | Default sender number for messages that don't set their own. |
transport | Type<SmsTransport> | TwilioTransport | Transport class used to deliver. |
transportInstance | SmsTransport | — | A pre-built transport instance (takes precedence over transport). Use for transports needing constructor args, e.g. a resilient failover transport. |
twilio | { accountSid, authToken, from? } | — | Credentials for the built-in TwilioTransport. |
vonage | { apiKey, apiSecret, from? } | — | Credentials for the built-in VonageTransport. |
sns | { region, credentials?, senderId? } | — | Config for the built-in AWS SnsTransport. |
resolveTransport | (tenant: string) => SmsTransport | — | Per-tenant transport resolver. See Per-tenant config. |
global | boolean | true | Register globally so the channel is discoverable app-wide. |
Three transports ship in the box — pick one with transport and supply its matching options:
import { VonageTransport, SmsChannelModule } from '@dudousxd/nestjs-notifications-sms';
SmsChannelModule.forRoot({
transport: VonageTransport,
vonage: { apiKey: process.env.VONAGE_KEY!, apiSecret: process.env.VONAGE_SECRET!, from: 'Acme' },
});import { SnsTransport, SmsChannelModule } from '@dudousxd/nestjs-notifications-sms';
SmsChannelModule.forRoot({
transport: SnsTransport,
sns: { region: 'us-east-1', senderId: 'Acme' }, // credentials from the AWS default chain
});Each transport pulls its own SDK as an optional peer (twilio, @vonage/server-sdk,
@aws-sdk/client-sns) — install only the one you use.
The notification side
Annotate the payload method with the @Sms() handle and return a string:
import { type Notifiable, Notification } from '@dudousxd/nestjs-notifications-core';
import { Sms } from '@dudousxd/nestjs-notifications-sms';
@Notification()
export class LoginCode {
constructor(private code: string) {}
@Sms()
toSms({ notifiable }: ChannelContext): string {
return `Your code is ${this.code}`;
}
}The Sms handle also works as a via() token (via() { return [Sms]; }) for
explicit routing; implement
SmsNotification alongside the decorator for compile-time checks on toSms().
SmsMessage builder
Return an SmsMessage when you need to override the sender number for a single notification. Each
method returns this:
import { Sms, SmsMessage } from '@dudousxd/nestjs-notifications-sms';
@Sms()
toSms({ notifiable }: ChannelContext): SmsMessage {
return new SmsMessage()
.content(`Your code is ${this.code}`)
.from('+15555550199');
}| Method | Effect |
|---|---|
.content(text) | Set the SMS body text. |
.from(number) | Override the sender number for this message. |
A message's from wins over the module's from, which in turn falls back to the transport's own
default sender.
Routing the recipient
The recipient comes from routeNotificationFor('sms') — return the phone number to text:
export class User implements Notifiable {
constructor(public phone: string) {}
routeNotificationFor(channel: string) {
if (channel !== 'sms') return undefined;
return this.phone;
}
}Custom transports
A transport is one method — implement SmsTransport and pass it as transport:
import { Injectable } from '@nestjs/common';
import type { SmsTransport, SmsTransportPayload } from '@dudousxd/nestjs-notifications-sms';
@Injectable()
export class VonageTransport implements SmsTransport {
async send(payload: SmsTransportPayload): Promise<void> {
// payload is { to, from?, text }
await myVonageClient.send(payload.to, payload.from, payload.text);
}
}SmsChannelModule.forRoot({ from: '+15555550100', transport: VonageTransport });A custom transport is a normal Nest provider, so you can inject config or an HTTP client into it.
The twilio option is ignored unless you use the default TwilioTransport.
Multi-provider failover
resilientTransport (from @dudousxd/nestjs-notifications-resilience)
wraps an ordered list of transports and tries each until one succeeds — so when Twilio is down, the
next provider (say Vonage) takes over. Beyond plain try-in-order, it adds a per-provider circuit
breaker (a known-dead provider is skipped instantly instead of retried every send) and a
per-attempt timeout (a slow provider is treated as a failure). Pass it as a pre-built instance
via transportInstance (which takes precedence over the transport class):
pnpm add @dudousxd/nestjs-notifications-resilience @dudousxd/nestjs-resilienceimport { TwilioTransport, VonageTransport } from '@dudousxd/nestjs-notifications-sms';
import { resilientTransport } from '@dudousxd/nestjs-notifications-resilience';
SmsChannelModule.forRoot({
from: '+15555550100',
transportInstance: resilientTransport(
[
{ id: 'twilio', transport: new TwilioTransport(twilioOpts) },
{ id: 'vonage', transport: new VonageTransport(vonageOpts) },
],
{
keyPrefix: 'sms', // circuit keys: 'sms:twilio', 'sms:vonage'
timeoutMs: 8_000, // per-attempt timeout
breaker: { threshold: 5, cooldownMs: 30_000 }, // open after 5 failures, probe after 30s
onFailover: (id, error) => logger.warn(`sms provider ${id} failed over: ${error}`),
},
),
});The last error is rethrown only if every provider fails. By default breaker state is per-process;
pass a distributed @dudousxd/nestjs-resilience-store-*
adapter as store to share it fleet-wide. The same resilientTransport works for the mail channel —
it's generic over any { send(payload) } transport.
Per-tenant config
In a multi-tenant app each tenant can text from its own provider/credentials. Pass
resolveTransport — when a send is scoped to a tenant (via
forTenant(id) or a @Tenant() property), the channel uses the
SmsTransport you return instead of the default:
SmsChannelModule.forRoot({
from: '+15555550100',
resolveTransport: (tenant) => tenantSmsTransports.get(tenant) ?? defaultTransport,
});Sends with no tenant scope keep the default transport. See Multi-tenancy for the full picture.
Webhook
Deliver notifications as an HTTP request to any endpoint. Return a plain object for a JSON POST, or a fluent WebhookMessage for full control over url, method, and headers.
Push
Send push notifications through Web Push, Firebase Cloud Messaging, or Expo. Pick one transport, build a PushMessage, and route to one device token or many.