GenerateSaaS

SMS

Send rate-limited broadcast and admin-test text messages through Twilio or SNS with the @repo/sms package.

@repo/sms sends text messages through a swappable provider, gated by config.sms in packages/config/src/index.ts. When enabled (the shipped default, provider twilio) the backend exposes a queued sendSMS() helper and registers the delivery worker; when off, every call site short-circuits and phone-related UI disappears.

What you get

  • A queued sendSMS(to, body) helper - enqueues, never hits the provider directly.
  • A throttled, retrying background worker (sendSMSFunction).
  • Two real SMS surfaces: broadcast announcements and an admin [TEST] send.
  • Two interchangeable providers: Twilio (default) and Amazon SNS.

There is no generic per-user transactional SMS channel and no SMS 2FA - the second factor is TOTP + backup codes only. SMS powers only broadcasts and the admin test send.

Configuration

config.sms is a discriminated union: { enabled: false } or { enabled: true; provider; limitPerSecond? }.

KeyTypeDefaultDescription
enabledbooleantrueMaster flag; false unregisters the worker and hides phone UI.
provider"twilio" | "sns""twilio"Delivery backend; Amazon SNS is the alternative.
limitPerSecondnumber?10Caps outbound throughput (worker throttle).
packages/config/src/index.ts
sms: {
  enabled: true,
  provider: "twilio",
  limitPerSecond: 10
}

Credentials live in environment variables, not in config.

ProviderEnv vars
TwilioTWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER
Amazon SNSAMAZON_SNS_REGION, AMAZON_SNS_KEY, AMAZON_SNS_SECRET, optional AMAZON_SNS_SENDER_ID, AMAZON_SNS_ORIGINATION_NUMBER

See Environment Variables.

Sending an SMS

sendSMS(to, body) (packages/sms/src/send.ts) is the call you make from app code. It enqueues an sms/send event so delivery is retried and rate-limited in the worker - it does not reach the provider inline.

import { sendSMS } from "@repo/sms";

await sendSMS("+15551234567", "Your code is 123456");

getSMSProvider() lazily imports and caches the provider class (TwilioSMSProvider or SNSSMSProvider), throwing when config.sms.enabled is false. The worker uses it - feature code does not.

Rate limiting

The worker sendSMSFunction (packages/api/src/functions/notifications/sms.ts) is throttled to config.sms.limitPerSecond events per second (throttle: { limit, period: "1s" }, default 10). It retries failed sends up to 3 times, protecting your provider account from bursts and absorbing transient failures.

It is only registered in packages/api/src/routes/inngest.ts when config.sms.enabled is true. For HTTP-layer limits unrelated to SMS, see Caching.

What SMS actually sends

createNotification only inserts an in-app row - SMS is sent on exactly two paths, both gated by config.sms?.enabled (notifications service, packages/notifications/src/service.ts).

SurfaceTriggerTargets
Broadcastconfig.sms.enabled and config.notifications.enabled; createBroadcast queues announcement/broadcast.smsUsers who are not banned, have marketingOptIn, and have a phone number (numbers deduplicated, HTML stripped)
Admin test sendsendTestAnnouncement, SMS channel selected and target user has a phone numberOne [TEST]-prefixed message to a single user

Broadcast concurrency is config.notifications.broadcastConcurrency (default 5). If either flag is off, the worker is not registered and the admin announcements composer hides its SMS channel. See Background jobs.

Adding a provider

Both providers implement the SMSProvider interface from packages/sms/src/types.ts (send(payload) => Promise<{ id }>).

Create a class in packages/sms/src/providers/ implementing SMSProvider.
Add the provider literal to the SMSProvider union in packages/config/src/types/index.ts.
Add a case for it in the switch inside getSMSProvider() (packages/sms/src/send.ts) - the exhaustiveCheck enforces this at build time.
Register any new credential env vars in packages/runtime/src/env.ts (providers read from env, not config).

Frequently asked questions

Can I use SMS for two-factor authentication? No. Toggling SMS has no effect on 2FA, which is TOTP + backup codes only. See Two-factor & passkeys.

Why didn't my sendSMS() call deliver instantly? It enqueues an sms/send event; the throttled worker delivers it, retrying up to 3 times. Verify the worker is registered (config.sms.enabled is true).

Which users receive a broadcast SMS? Only users who are not banned, have marketingOptIn, and have a stored phone number. Numbers are deduplicated before sending.

Phone fields disappeared from the UI - why? When config.sms.enabled is false, every SMS call site short-circuits and phone-related fields and the announcement SMS channel are hidden.

On this page