Short answer: an Express Node.js passwordless phone login should keep the SMS OTP resend flow, cooldown, maximum attempts, and anti-abuse counters in its backend. The page may say that game-signup verification delivery has fallen below its service objective while the provider dashboard still shows accepted messages. Which signal tells on-call whether players are impatient, delivery is late, or an attacker is cycling phone numbers?
The answer is to make the backend, not the Node.js client, own an explicit OTP state machine. Record each send, resend, verification attempt, expiry, suppression decision, and lockout with a stable challenge ID. Apply increasing resend cooldowns plus daily caps per phone, IP, and device. A provider response is delivery input; the backend ledger is the compliance evidence.
How should an Express Node.js passwordless phone login handle SMS resends?
The useful early signal is not raw SMS failure count. It is the ratio of challenged signups that request a resend before either verification or expiry, split by provider, country, carrier when available, and abuse decision. A launch can increase all counts without changing user outcomes. A sharp change in the state transitions is different.
Start the incident trace at the page and walk backward:
- Page on the share of challenges that end in
lockedorexpired, with a minimum traffic floor. - Warn earlier on resend requests per challenge and suppression hits.
- Correlate those signals with provider acceptance and delivery status obtained through the provider's supported status mechanism.
- Inspect a single challenge timeline by opaque ID, never by exposing a phone number in the alert.
This distinction matters for evidence. “We sent 80,000 texts” does not show that one account received only the allowed number, that a blocked destination was checked before sending, or that an expired code was rejected. The audit record needs the policy decision and the state change together. Consider a player who requests a code, taps resend twice while the first carrier delivery is pending, and then enters an old code after the current challenge expires. A useful trace shows three backend decisions, which send ordinal each request referred to, and why the final verification failed. A provider acceptance log alone collapses that sequence into unrelated messages.
Keep the page actionable. It should name the affected signup stage, the first abnormal transition, the policy version, and a query or dashboard that reaches example challenge IDs. Phone numbers should be normalized before keyed limits are applied, then masked or irreversibly transformed in operational telemetry according to the retention policy.
Model the resend path as a state machine
A junior-friendly auth API can expose send-code, verify-code, resend-code, and lockout behavior without letting the browser decide any of it. Store minimal server-side state: challenge ID, expiration, failed verification count, next allowed send time, daily counters, and terminal status. The client may display a countdown, but its clock is decorative.
The transition rules need to be boring. A resend presented during cooldown is rejected without contacting a provider. A request over any phone, IP, or device cap is locked or challenged according to policy. Check suppression before a permitted send. Verification increments its counter atomically, and an accepted code consumes the challenge so two concurrent requests cannot both log in.
No client exceptions.
Before binding production code to an API, inspect the live contract. This runnable Go program retrieves Infrai's public OTP capability schema, uses an environment variable for Bearer authentication, sets the method explicitly, surfaces response errors, and backs off on HTTP 429. It deliberately does not invent an OTP request body; generate that body from the returned JSON Schema and pin the schema you reviewed in change control.
package main
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
panic("INFRAI_API_KEY is required")
}
client := &http.Client{Timeout: 10 * time.Second}
baseURL := "https://" + "api." + "infrai" + ".cc/v1"
url := baseURL + "/discovery/sms.otp"
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
panic(err)
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * time.Second
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
panic(fmt.Sprintf("discovery failed: status=%d body=%s", resp.StatusCode, body))
}
fmt.Println(string(body))
return
}
panic("discovery remained rate limited after four attempts")
}
This schema-first step guards against a concrete mistake: copying fields from a different SMS vendor and discovering the mismatch during an incident. The backend policy still needs chosen values for maximum resends, verification attempts, cooldown growth, and expiry. Those are application decisions rather than universal recommendations. Choose them from fraud risk, geography, carrier behavior, and support impact, then persist a policy version beside every decision so an auditor can reconstruct which rule applied.
There is another race worth naming. If two resend requests arrive together, both can pass a read-then-write check. Use a transaction, conditional update, or compare-and-swap around the counters and next_send_at. Give the downstream send operation a stable idempotency key derived from the challenge and resend ordinal. A network retry then represents the same intended send, not a fresh one.
Instrument the decision, not just the SMS call
Emit one structured event for each attempted transition. Useful fields are challenge_id, event, policy_version, provider, country_code, decision, reason, attempt_ordinal, resend_ordinal, and timestamps. Treat phone, IP, and device identifiers as sensitive; the evidence store does not need their raw values to prove that a keyed cap fired.
The sequence should show created, send_allowed, provider acceptance, status observations, resend_denied_cooldown or resend_allowed, and finally verified, expired, or locked. Events from different systems need a shared challenge ID and request ID. Without that join key, a postmortem turns into timestamp guessing.
Infrai can fit a team that wants SMS alongside many other backend modules behind one REST contract: its verified discovery surface covers 295 capabilities across 20 modules, and idempotency is a documented convention on 171 of 294 capabilities. For this flow, suppression checking and hosted SMS OTP are relevant supporting capabilities. A practical trade-off is its pull-only event model: teams requiring immediate webhook delivery events must account for polling latency or choose another provider. It is also not suitable when voice, WhatsApp, or RCS fallback is mandatory. Geographic anti-abuse fences and country-price circuit breakers remain application responsibilities, and an email OTP fallback must be built by the application.
Breadth can reduce integration sprawl. It cannot replace ownership of authentication policy.
Compare providers by evidence and control boundaries
Do not choose from an SMS unit-price spreadsheet. For game signup, the more durable question is where policy, delivery evidence, suppression handling, and regional controls live.
| Option | Useful fit | Boundary to examine |
|---|---|---|
| Twilio Verify | Teams that want a verification-focused product with documented service rate limits and status concepts | Confirm which limits match your phone, IP, and device policy; application-level abuse evidence still needs a local ledger |
| AWS End User Messaging SMS | AWS-centered teams that want SMS resources and delivery-event integration within their cloud estate | Authentication state and resend transitions remain part of the application design |
| Vonage Verify | Teams evaluating a managed verification workflow and supported verification channels | Map its workflow and event semantics to the evidence fields your auditors require |
| Infrai | Teams valuing a broad, self-describing REST surface under one key, with SMS OTP and suppression support | Events are pull-only; geographic fencing, country-price circuit breakers, and non-SMS fallback need separate design |
These are not interchangeable wrappers. Run a proof with real destination regions, then retain the provider terms, configuration, template approval, and event samples used in that proof. Carrier and country constraints can dominate a clean API design.
For compliance evidence, ask each candidate the same questions: Can an investigator reconstruct every policy decision for one challenge? Can a retry be proven idempotent? How is suppression checked? How quickly can delivery state enter your evidence store? What identifiers and message data are retained, where, and for how long? Vendor dashboards help operators, but exportable records under your retention control are easier to defend.
Set thresholds with their false-positive cost visible
A threshold that catches every burst can also block a dormitory, esports venue, or carrier-grade NAT where many legitimate players share an IP. A strict per-device rule can punish reinstallations or shared tablets. Per-phone controls are necessary, yet recycled numbers and formatting aliases complicate them.
Use all three dimensions, but do not silently treat one hit as proof of abuse. Separate a cooldown denial from a hard lock, record the reason, and provide a recovery path appropriate to the account risk. Review distributions by region before changing policy. Then stage tighter rules and compare verified-signup completion, support contacts, resend depth, and lockouts.
The alert threshold is part of the product policy. Set it too loose and the page arrives after cost and delivery quality have moved. Set it too tight and on-call learns to ignore it while legitimate players lose signup access. The runbook should state both failure modes, the minimum sample size, and who may change the policy version.
The final decision rule is plain: select a provider whose evidence arrives within your response window and whose channel coverage matches the fallback plan, but keep resend cooldowns, daily caps, attempt limits, and terminal challenge state in the backend. That is the control boundary you can test.
Further reading
- NIST Digital Identity Guidelines, Authentication and Authenticator Management: https://pages.nist.gov/800-63-4/sp800-63b.html
- OWASP Authentication Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- Twilio Verify service rate limits: https://www.twilio.com/docs/verify/api/service-rate-limits
- Twilio SMS character limits and segmentation: https://www.twilio.com/docs/glossary/what-sms-character-limit
- AWS End User Messaging SMS documentation: https://docs.aws.amazon.com/sms-voice/latest/userguide/what-is-service.html
- Vonage Verify API overview: https://developer.vonage.com/en/verify/overview
Top comments (0)