services.sms
Outbound SMS delivery through pluggable providers (Aliyun SMS, Twilio).
services.sms
- Stability:
stable - Canonical source:
packages/spec/src/contracts/sms-service.ts - Registry slot:
sms— resolve withctx.getService('sms').
Methods
services.sms.send(input: SendSmsInput): Promise<SendSmsResult>
services.sms.isConfigured(): booleanSendSmsInput carries the recipient (to, E.164 recommended), the rendered
body, and optional provider-template fields (templateId,
templateParams) for template-only providers such as Aliyun SMS — which
refuse free-form bodies and only deliver pre-registered templates.
isConfigured() is false while the service runs on the development
LogSmsTransport fallback (messages are logged, never delivered). Consumers
that advertise SMS-dependent features — phone OTP sign-in, SMS invitations —
gate on it in production (features.phoneNumberOtp).
Returns
SendSmsResult:
id: string— correlation id for the attemptstatus: 'sent' | 'failed'messageId?: string— provider message id (Aliyun BizId, Twilio SID)error?: string— transport detail whenstatus='failed'
No persistence, no body logging
Unlike services.email (which persists to sys_email), the SMS service
deliberately has no persistence surface and never logs message bodies:
SMS bodies routinely carry one-time passwords, so a message log would be a
credential store. Diagnostics log masked recipients and statuses only.
Configuration
Providers are configured through the sms settings namespace
(Setup → Settings → SMS Delivery), with live rebinding — no restart needed:
provider—log(default, no real delivery) /aliyun/twilio- Aliyun:
aliyun_access_key_id,aliyun_access_key_secret(encrypted),aliyun_sign_name,aliyun_template_code(default catch-all template with a single${content}variable for generic notification SMS) - Twilio:
twilio_account_sid,twilio_auth_token(encrypted), andtwilio_from_numberortwilio_messaging_service_sid
Every key accepts the standard settings env override (OS_SMS_PROVIDER,
OS_SMS_ALIYUN_ACCESS_KEY_ID, …). The Send test SMS action exercises the
live (or unsaved) provider configuration.
OS_SMS_PROVIDER (and config.sms.provider) is read twice, at two different
moments: once by os serve while it assembles the kernel — before any settings
service exists — to pick the plugin's initial transport, and again by the
settings namespace once that binds at kernel:ready. A provider value outside
log / aliyun / twilio is refused at the first of those (#5713): the boot
fails when the app declares requires: ['sms'], and otherwise logs an error and
starts without an SMS service. It used to become the log transport silently, so
a typo like OS_SMS_PROVIDER=twilo produced a server that answered every send
status: 'sent' and delivered nothing. Credentials are not required at boot —
only the provider tag is checked, because the credentials legitimately arrive
later, from this namespace. An environment that is not meant to send SMS says so
with OS_SMS_PROVIDER=log, which is the default.
Consumers
- Phone-number OTP auth — sign-in verification codes and self-service
password-reset codes (
/phone-number/*, see Authentication). - Messaging
smschannel —notify(channels:['sms'])resolves the recipient'ssys_user.phone_number, renders the(topic, 'sms', locale)row fromsys_notification_template, and delivers through this service with outbox retry/dead-letter. - Identity import SMS invitations — the
invitepassword policy sends a credential-free invitation SMS to phone-only rows.