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? }.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Master flag; false unregisters the worker and hides phone UI. |
provider | "twilio" | "sns" | "twilio" | Delivery backend; Amazon SNS is the alternative. |
limitPerSecond | number? | 10 | Caps outbound throughput (worker throttle). |
sms: {
enabled: true,
provider: "twilio",
limitPerSecond: 10
}Credentials live in environment variables, not in config.
| Provider | Env vars |
|---|---|
| Twilio | TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER |
| Amazon SNS | AMAZON_SNS_REGION, AMAZON_SNS_KEY, AMAZON_SNS_SECRET, optional AMAZON_SNS_SENDER_ID, AMAZON_SNS_ORIGINATION_NUMBER |
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).
| Surface | Trigger | Targets |
|---|---|---|
| Broadcast | config.sms.enabled and config.notifications.enabled; createBroadcast queues announcement/broadcast.sms | Users who are not banned, have marketingOptIn, and have a phone number (numbers deduplicated, HTML stripped) |
| Admin test send | sendTestAnnouncement, SMS channel selected and target user has a phone number | One [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 }>).
packages/sms/src/providers/ implementing SMSProvider.SMSProvider union in packages/config/src/types/index.ts.case for it in the switch inside getSMSProvider() (packages/sms/src/send.ts) - the exhaustiveCheck enforces this at build time.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.