Provider-agnostic SMS OTP delivery with E.164 phone number validation, per-window rate limiting, and bcrypt-hashed code storage. Supports Twilio, AWS SNS, and Vonage. OTP codes are never stored in plaintext — only their bcrypt hash is persisted, so a database breach does not expose active codes.
nself plugin install nself-sms
nself build
nself start# Twilio (default provider)
SMS_PROVIDER=twilio
SMS_TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxx
SMS_TWILIO_AUTH_TOKEN=your-auth-token
SMS_TWILIO_FROM_NUMBER=+15550001234
# AWS SNS
SMS_PROVIDER=aws-sns
SMS_AWS_REGION=us-east-1
SMS_AWS_ACCESS_KEY_ID=AKIA...
SMS_AWS_SECRET_ACCESS_KEY=your-secret
# Vonage
SMS_PROVIDER=vonage
SMS_VONAGE_API_KEY=your-api-key
SMS_VONAGE_API_SECRET=your-api-secret
SMS_VONAGE_FROM_NUMBER=+15550001234| Variable | Required | Default | Description |
|---|---|---|---|
SMS_PROVIDER | Yes | twilio | SMS provider: twilio, aws-sns, vonage |
SMS_OTP_TTL_MINUTES | No | 10 | OTP validity window in minutes before expiry |
SMS_OTP_MAX_ATTEMPTS | No | 3 | Maximum verification attempts before the code is invalidated |
SMS_RATE_WINDOW_MINUTES | No | 15 | Rate limit sliding window in minutes |
SMS_RATE_MAX_SENDS_PER_WINDOW | No | 5 | Max sends per phone number per window |
SMS_RATE_MAX_SENDS_PER_HOUR | No | 10 | Hard hourly cap per phone number |
SMS_RATE_BLOCK_THRESHOLD_24H | No | 50 | 24-hour block threshold — number is suspended above this count |
SMS_TEST_MODE | No | false | When true, OTP codes are returned in the API response instead of sent (dev use) |
# 1. Request OTP — sends SMS to the user's phone
curl -X POST https://api.yoursite.com/sms/otp/send \
-H "Authorization: Bearer $TOKEN" \
-d '{"phone": "+14155550123"}'
# Returns: {"expires_at": "2026-05-01T10:10:00Z"}
# 2. Verify OTP — check the code the user received
curl -X POST https://api.yoursite.com/sms/otp/verify \
-H "Authorization: Bearer $TOKEN" \
-d '{"phone": "+14155550123", "code": "847261"}'
# Returns: {"verified": true}The plugin registers a Hasura Remote Schema exposing sendOtp and verifyOtp mutations. Frontend apps can call OTP flows directly via GraphQL with their JWT without hitting the REST endpoint.
OTP codes are generated using crypto/rand and stored only as bcrypt hashes. After SMS_OTP_MAX_ATTEMPTS failed verifications, the code entry is deleted and the user must request a new one. Rate limiting is enforced at three levels: per sliding window, per hour, and a 24-hour block that suspends a number showing abuse patterns. E.164 format is validated before any provider call.
Pro Plugin — ɳSelf+ | Port: 3824 | v1.0.0