SMS Providers
FORGE supports sending SMS messages and OTP verification through multiple provider drivers. The SMS system is used for phone number verification, two-factor authentication, and transactional notifications.
SmsProvider Trait
All SMS drivers implement the SmsProvider trait:
#[async_trait]
pub trait SmsProvider: Send + Sync {
/// Send a plain text SMS message
async fn send(&self, to: &str, message: &str) -> Result<(), SmsError>;
/// Send a one-time password via the provider's verification service
async fn send_otp(&self, to: &str, code: &str) -> Result<(), SmsError>;
/// Verify an OTP code entered by the user
async fn verify_otp(&self, to: &str, code: &str) -> Result<bool, SmsError>;
}Slack Integration (Development/Testing)
When SMS_SLACK_ENABLED=true, all SMS messages are intercepted and sent to a Slack channel instead of the actual provider. This is useful for:
- Development: See OTP codes without needing a real phone
- Testing: Verify SMS content in your Slack workspace
- Monitoring: Track all SMS traffic in a dedicated channel
SMS_SLACK_ENABLED=true
┌─────────┐ ┌───────────────┐ ┌─────────────┐
│ App │───▶│ SlackSmsProxy │───▶│ Slack │
│ sends │ │ (intercepts) │ │ Channel │
│ SMS │ └───────────────┘ └─────────────┘
└─────────┘
SMS_SLACK_ENABLED=false (default)
┌─────────┐ ┌─────────────────┐ ┌───────────────┐
│ App │───▶│ Twilio/Unifonic │───▶│ Real SMS │
│ sends │ │ Provider │ │ to Phone │
│ SMS │ └─────────────────┘ └───────────────┘
└─────────┘Configuration:
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
SMS_SLACK_ENABLED | boolean | false | No | Send SMS to Slack instead of provider |
SMS_SLACK_WEBHOOK_URL | string | - | Yes* | Slack incoming webhook URL |
SMS_SLACK_CHANNEL | string | - | No | Optional channel override |
*Required when SMS_SLACK_ENABLED=true. The app will fail at startup if missing.
Setup:
- Create an Incoming Webhook in your Slack workspace
- Set the environment variables:
SMS_SLACK_ENABLED=true
SMS_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXX
SMS_SLACK_CHANNEL=#sms-logs # Optional- SMS messages will now appear in Slack:
*SMS Message*
:iphone: To: `+1234567890`
:envelope: Message:
Your verification code is: 123456
:id: ID: `uuid-here`WARNING
The Slack proxy cannot verify OTPs - verification must be done via your database. This is the same behavior as the log driver.
Available Drivers
Slack (Development)
Send SMS/OTP messages to a Slack channel instead of real phones. Perfect for development and testing without SMS costs.
Installation:
forge new --interactive
# Select "Slack (development)" when prompted for SMS providerOr via CLI:
forge new myapp --sms slackConfiguration:
| Variable | Type | Required | Description |
|---|---|---|---|
SMS_DRIVER | string | Yes | Set to slack |
SMS_SLACK_WEBHOOK_URL | string | Yes | Slack incoming webhook URL |
SMS_SLACK_CHANNEL | string | No | Optional channel override |
Setup:
- Create an Incoming Webhook in your Slack workspace
- Configure your
.env:
SMS_DRIVER=slack
SMS_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXX
SMS_SLACK_CHANNEL=#otp-codes # Optional- OTP codes will appear in Slack:
📱 SMS to +1234567890
Your verification code is: 123456. This code expires in 10 minutes.TIP
Use Slack for development to avoid SMS costs and see OTP codes instantly in your team channel.
WARNING
The Slack provider cannot verify OTPs server-side - verification is done via your database (same as Log driver).
Twilio
Full-featured SMS and OTP provider using Twilio's REST API and Verify service.
Installation:
forge provider:add sms:twilioConfiguration:
| Setting | Type | Description |
|---|---|---|
twilio_account_sid | string | Twilio Account SID |
twilio_auth_token | encrypted | Twilio Auth Token |
twilio_from_number | string | Sender phone number (E.164 format) |
twilio_verify_sid | string | Twilio Verify Service SID |
TIP
The twilio_verify_sid is required for OTP functionality. Create a Verify Service in the Twilio Console to obtain this value.
How OTP works with Twilio:
1. send_otp() → Calls Twilio Verify API to send code
2. User enters code in your app
3. verify_otp() → Calls Twilio Verify API to check code
4. Returns true/falseUnifonic
SMS provider popular in the Middle East and North Africa region.
Installation:
forge provider:add sms:unifonicConfiguration:
| Setting | Type | Description |
|---|---|---|
unifonic_app_sid | encrypted | Unifonic Application SID |
unifonic_sender_id | string | Sender ID (alphanumeric or number) |
TIP
Unifonic sender IDs may require registration with local telecom authorities depending on the destination country.
Vonage
Global SMS provider (formerly Nexmo).
Installation:
forge provider:add sms:vonageConfiguration:
| Setting | Type | Description |
|---|---|---|
vonage_api_key | string | Vonage API Key |
vonage_api_secret | encrypted | Vonage API Secret |
vonage_from | string | Sender ID or number |
Usage
Creating the Provider
The factory creates the correct driver based on your configuration:
use crate::services::sms::SmsFactory;
let sms = SmsFactory::create(&settings).await?;Sending a Message
// Send a plain text SMS
sms.send("+1234567890", "Your order has been shipped!").await?;OTP Verification
// Step 1: Send an OTP to the user's phone
sms.send_otp("+1234567890", "123456").await?;
// Step 2: User enters the code in your app
// Step 3: Verify the code
let is_valid = sms.verify_otp("+1234567890", "123456").await?;
if is_valid {
// Phone number verified
} else {
// Invalid code
}WARNING
Phone numbers must be in E.164 format (e.g., +1234567890). Sending to numbers without the country code prefix will fail with most providers.
Configuration via Admin
All SMS settings are managed through the Settings admin page under the SMS group. Credentials are stored as encrypted settings and masked in the admin interface.
Settings > SMS
┌──────────────────────────────────────────────────┐
│ Provider: [Twilio ▼] │
│ Account SID: [ACxxxxxxxxxx ] │
│ Auth Token: [•••••••••••••••• ] │
│ From Number: [+15551234567 ] │
│ Verify SID: [VAxxxxxxxxxx ] │
└──────────────────────────────────────────────────┘Error Handling
All SMS operations return Result<(), SmsError> with structured error types:
match sms.send("+1234567890", "Hello").await {
Ok(()) => println!("Message sent"),
Err(SmsError::InvalidNumber) => println!("Bad phone number format"),
Err(SmsError::ProviderError(msg)) => println!("Provider error: {}", msg),
Err(SmsError::RateLimited) => println!("Too many requests"),
Err(e) => println!("Unexpected error: {}", e),
}