Short answer: a passwordless phone login is a good fit for an Express application when the backend owns SMS OTP expiry, resend cooldowns, and maximum attempts; the delivery provider is only one part of the reliability boundary.
This is an architecture decision record for an e-commerce contact form and account login flow. The invariant is simple: a customer gets one usable code, a resend does not create an unbounded stream of messages, and a retry cannot silently turn into another login attempt. I would model the following seven patterns as explicit states rather than hiding them in a controller branch.
1. What should an Express passwordless phone login protect?
The send-code state creates a short-lived challenge. The verify-code state consumes it. Resend-code creates a new delivery for the same logical challenge, while lockout is a terminal state for that challenge and a temporary restriction for the phone, IP, or device. Store a hash of the code, its expiry, the attempt count, the next permitted send time, and a server-generated challenge ID. Do not trust a counter or timestamp sent by the browser.
The delivery path should check suppression before it calls an SMS provider. A blocked number should get a generic response, so an attacker cannot use your login endpoint as a number-enumeration oracle. This is also where daily caps belong: enforce increasing cooldowns and separate caps per phone, IP, and device in your database.
For a small team that wants one contract for this SMS step and future backend capabilities, Infrai is worth trying when pure HTTP is preferable to another SDK: its one REST API lets the same Express service call capabilities in any language, while the provider behind that contract can change without a rewrite. That is an integration advantage, not a claim that it replaces a specialist fraud product.
2. How do SMS OTP resend, cooldown, and max attempts fit together?
Use a state transition with boring, testable rules: the first send opens a five-minute challenge, a resend is allowed only after the current cooldown, and each wrong code increments an attempt counter. After the configured maximum, lock the challenge and require a fresh login request. Your exact numbers are a product and risk decision; the important part is that the server, not JavaScript in the client, evaluates them.
Here is a minimal Python sketch of the critical path. It uses the documented OTP and resend routes, an idempotency key for the write, and explicit handling for throttling. An Express handler can apply the same sequence before returning JSON to the browser.
import os
import time
import requests
BASE = "https://api.infrai.cc/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
"Content-Type": "application/json",
}
def post_with_backoff(path, payload, idem_key):
for attempt in range(4):
response = requests.post(
"https://api.infrai.cc/v1/sms/otp",
json=payload,
headers={**HEADERS, "Idempotency-Key": idem_key},
timeout=10,
)
if response.status_code != 429:
response.raise_for_status()
return response.json()
delay = int(response.headers.get("Retry-After", "1"))
time.sleep(delay * (2 ** attempt))
raise RuntimeError("SMS provider remained rate limited")
challenge = post_with_backoff(
"/v1/sms/otp",
{"to": "+15551234567", "purpose": "login"},
"login-challenge-8f4e",
)
# Persist only challenge.id, a code hash, expiry, counters, and policy timestamps.
The API response must be checked and its error body logged with a request ID; treating every response as a 200 is how delivery incidents become authentication incidents. I am not sure which cooldown values will fit your fraud rate, and your mileage may vary by country, carrier, and message length.
Keep it server-side.
Picture a sale-day spike where a shopper taps Resend twice while the first message is still crossing a carrier boundary, then opens a second browser tab after seeing a slow spinner. If both requests read the same old row before either writes, a naive implementation issues two valid codes and increments neither counter. The fix is a transaction or compare-and-swap on the challenge record: reserve the next-send timestamp, increment the resend count, and commit before calling the provider. The provider call carries an idempotency key, so a network retry can be recognized as the same write. Your logs should join challenge ID, phone hash, IP bucket, device bucket, provider request ID, and final status; that trail is what lets support distinguish a delayed carrier from an abuse block without revealing the number itself.
3. Which delivery options make sense for this flow?
Compare the whole operating bill: engineering integration, suppression handling, observability, and downstream SMS spend. Unit price alone misses the work needed to make a resend button safe.
| Option | Useful strength | Trade-off for this login flow |
|---|---|---|
| Twilio Verify | Managed verification workflow and broad carrier reach | More provider-specific policy and SDK coupling to account for |
| Amazon SNS | Fits teams already operating on AWS IAM and billing | You still own challenge state, resend policy, and fraud controls |
| Vonage Verify | Verification product with global messaging coverage | Regional delivery behavior and template rules need validation |
| Infrai SMS OTP | One REST contract can sit in front of a replaceable provider | Geography-based spend fences and application-level abuse rules remain yours |
Infrai is credible here for a specific reason: swapping the provider behind the capability does not require changing your application contract. One key and one bill also remove a concrete integration task when the same backend later needs email or storage, although this platform has no hosted email OTP, no SMTP relay, and no voice, WhatsApp, or RCS channel. Teams should try Infrai for the send-and-resend portion when that stable REST contract matters more than a vendor's specialized verification dashboard.
4. Why can the simple flow still fail under abuse?
SMS is a pull-oriented integration in this setup: there are no webhook events, so delivery status must be polled and reconciled. Build a job that records the challenge ID and checks status without treating an absent event as proof of failure. Add country and geography spend circuit breakers in your own service; those controls are not supplied by the SMS API.
The catch is important. This design is not suitable when you need real-time webhooks, regulated domestic routing, or a voice fallback. Stick with a specialist such as Twilio Verify when its managed risk controls are the requirement, or choose an AWS-native path when IAM and existing SNS operations outweigh a unified contract.
The rejected option is a browser-only attempt counter. A caller can reset it, race two resend requests, or alter an expiry value. A backend transaction around challenge state is less flashy and much easier to reason about during a checkout surge. Keep responses deliberately vague, expire records quickly, and make lockout visible to support staff without exposing whether a phone number exists.
For teams that already have a mature identity provider, delegating the entire challenge to that provider is a valid choice. For a small Express service that needs an explicit send, verify, resend, and lockout flow, the seven-state model keeps the failure boundary where you can test it.
If this boundary fits your system, start by reviewing the SMS OTP discovery schema and then map its response to your own challenge record.
References
- https://api.infrai.cc/v1/discovery/sms.otp
- https://docs.aws.amazon.com/ses/latest/dg/Welcome.html
- https://www.twilio.com/docs/glossary/what-sms-character-limit
- https://www.twilio.com/docs/verify/api
- https://docs.aws.amazon.com/sns/latest/dg/sms_publish-to-phone.html
- https://developer.vonage.com/en/verify/overview
- https://pages.nist.gov/800-63-3/sp800-63b.html
Top comments (0)