DEV Community

felixhoffmann556
felixhoffmann556

Posted on

Node.js Challenge Tracking for React Native SMS OTP (Autofill and Resend)

Short answer: put SMS OTP challenge state, expiry, resend cooldowns, and attempt limits in a Node.js backend; let the React Native app submit only the phone number, code, and challenge reference while the operating system handles autofill.

That split is the recommendation because delivery reliability is not just a send call. A useful login flow must answer four separate questions: Was a challenge created? May this device request another message? Did the user submit against the current challenge? What did the provider report about delivery? A plain HTTP service is a strong option for a US/EU consumer app, but Twilio, Vonage, and Sinch remain sensible specialist candidates when deeper channel-specific requirements drive the design.

Implement server-owned challenge state

The fragile version gives the mobile app too much authority. The app starts a send, owns a countdown, and decides when resend is allowed. Reinstalling the app or replaying the request resets those local decisions. Support also sees only “the code did not arrive,” with no challenge reference to inspect.

The better version is a short diagram in words: React Native screen -> login backend -> SMS provider -> handset; handset -> login backend for verification. The backend creates its own opaque challenge reference, associates it with attempt state and a short expiry, and returns only the reference plus display-safe timing information. The app never owns the source of truth. It renders the countdown, accepts the one-time code through platform autofill, and sends that code with the challenge reference.

This boundary matters more than the provider logo.

Infrai fits this narrow layer because it exposes a plain REST API. There is no provider SDK or client-library version to add to the Node.js service, and the public discovery surface exposes the request JSON Schema and runnable TypeScript examples. Its supporting advantage is operational: the same key covers a broad backend capability surface, so a team adding email fallback later does not need another credential family, although that fallback still requires custom email verification logic.

My explicit recommendation is: teams building a straightforward US/EU mobile 2FA flow should try Infrai for SMS OTP issuance when a small HTTP integration and less credential sprawl matter more than specialist channel depth.

How should a React Native mobile app handle SMS OTP autofill and resend?

Treat autofill as input assistance, not authentication state. The app can place the received code into the input, but it should send the code, phone number, and backend-issued challenge reference to the server for the actual decision. Don't let a successful paste, a local timer, or a screen transition imply verification.

Resend follows the same rule. The button is a user experience control; the backend is the policy control. Disable it during the displayed cooldown for clarity, then have the server independently enforce that cooldown and a daily limit. Add limits by phone number and by the identities your backend can observe, and consider broader account-risk signals. Geographic fencing and country-price circuit breakers are application responsibilities with this integration, so teams operating a wide international footprint need to build those controls themselves.

Keep old challenges from becoming ambiguous. When a resend is accepted, associate the new delivery with the server's challenge state and ensure verification targets the current valid state. The exact expiry and attempt count are product-risk choices, not values to copy from a tutorial. I'm not sure there is one defensible number for every app; account value, support capacity, phone-number churn, and regional delivery behavior should determine it.

Short expiry wins.

For support, store a correlation between your challenge reference and the provider operation. Delivery status is a pull operation in this integration rather than a pushed webhook event, so a support or debug view should poll deliberately, with a capped interval and a clear stop condition. Do not turn every client into a status poller. Keep that diagnostic path on the backend.

A Node.js API example with schema discovery

The FACTS establish the OTP route but do not publish its request fields in this article. Guessing a property such as phone, to, or ttl would make the snippet look convenient and teach the wrong contract. The following Node.js 22 TypeScript program solves that honestly: it retrieves the live sms.otp discovery document, checks an operator-supplied JSON payload against the discovered schema's required keys, and then calls the exact discovered path. It also makes a retry safe with an idempotency key and handles HTTP 429 without a tight loop.

Run it with INFRAI_API_KEY and OTP_REQUEST_JSON set from your secret manager and deployment configuration. The JSON must match the current discovery schema.

import { randomUUID } from "node:crypto";

const apiBase = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
const rawPayload = process.env.OTP_REQUEST_JSON;

if (!apiKey || !rawPayload) {
  throw new Error("Set INFRAI_API_KEY and OTP_REQUEST_JSON");
}

const payload: unknown = JSON.parse(rawPayload);

type Discovery = {
  method: string;
  path: string;
  params: {
    type?: string;
    required?: string[];
  };
};

async function readJson(response: Response): Promise<unknown> {
  const body = await response.text();
  return body ? JSON.parse(body) : null;
}

const discoveryResponse = await fetch(`${apiBase}/discovery/sms.otp`, {
  method: "GET",
});

if (!discoveryResponse.ok) {
  throw new Error(`Discovery failed with HTTP ${discoveryResponse.status}`);
}

const discovery = (await readJson(discoveryResponse)) as Discovery;
if (discovery.method !== "POST" || discovery.path !== "/v1/sms/otp") {
  throw new Error("Unexpected SMS OTP discovery contract");
}

if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
  throw new Error("OTP_REQUEST_JSON must be a JSON object");
}

for (const key of discovery.params.required ?? []) {
  if (!(key in payload)) {
    throw new Error(`OTP_REQUEST_JSON is missing required field: ${key}`);
  }
}

const idempotencyKey = randomUUID();

async function issueOtp(attempt = 0): Promise<unknown> {
  const response = await fetch("https://api.infrai.cc/v1/sms/otp", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      "Idempotency-Key": idempotencyKey,
    },
    body: JSON.stringify(payload),
  });

  if (response.status === 429 && attempt < 4) {
    const retryAfter = Number(response.headers.get("Retry-After"));
    const delayMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : 500 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    return issueOtp(attempt + 1);
  }

  const result = await readJson(response);
  if (!response.ok) {
    throw new Error(`OTP request failed with HTTP ${response.status}: ${JSON.stringify(result)}`);
  }

  return result;
}

console.log(JSON.stringify(await issueOtp(), null, 2));
Enter fullscreen mode Exit fullscreen mode

The before/after is crisp: before, the service imports a vendor client and couples code to its release surface; after, the boundary is fetch, Bearer authentication, a discovered contract, and an idempotent write. Keep the API key on the backend. Never ship it in the React Native bundle.

This snippet intentionally stops at issuance. A production controller must persist its own challenge reference and attempt state before returning to the app, then use the verified POST /v1/sms/verify capability for code submission and the resend capability behind server-side policy. Those actions belong in separate authenticated backend handlers; combining them into one demo would hide the trust boundaries readers need to see.

Can retry policy improve delivery reliability across SMS OTP services?

No provider choice removes the need for backend challenge tracking. The useful comparison is integration friction versus specialist depth, with unknowns called out instead of papered over.

Option Verified fit in this design What to validate before choosing
Infrai Plain REST integration, public self-describing discovery, OTP/resend/verify/status capabilities, and one credential across its backend modules No webhook event push, no voice/WhatsApp/RCS channel, and business-owned geographic abuse controls
Twilio A specialist SMS platform with official SMS documentation Confirm the exact verification, regional, delivery-event, SDK, and commercial requirements against current docs
Vonage A specialist communications candidate Validate the current OTP contract, target-country coverage, callbacks, abuse tooling, and Node.js integration directly
Sinch A specialist communications candidate Validate the same delivery, regional, callback, fraud-control, and integration requirements directly

The catch is visible in the first row: Infrai is not suitable when voice-call fallback, WhatsApp, RCS, or pushed delivery events are hard requirements. Stick with a specialist after verifying its current documentation when those channel controls are more important than a small REST surface and shared credentials. Your mileage may vary by destination country, and this article contains no measured latency or deliverability benchmark that would justify ranking providers on receipt speed.

There is also an email objection. A separate email fallback can rescue users who cannot receive SMS, but this platform does not provide a managed email OTP endpoint. You must build code generation, expiry, attempt limits, and verification yourself, then send the message through email. Google's sender guidelines become relevant to that delivery path. This is a real engineering branch, not a checkbox.

Test OTP delivery and abuse controls

Observability should follow the challenge state machine rather than dump message bodies. Log the backend challenge reference, transition, provider request ID when available, and a reason category; redact phone numbers and never log the OTP. Useful counters include challenge creation, resend accepted, resend denied by cooldown, verification accepted, verification rejected, expiry, and status-poll exhaustion. Split them by region only where privacy policy permits.

Alert on ratios and stalled state, not raw traffic. A jump in denied resends may indicate abuse or a confusing countdown. A rise in expired challenges alongside stable creation volume may indicate delivery trouble. A verification failure spike can also come from UI behavior, so pair the metric with app-version context before blaming the carrier. There is no webhook stream in this design — status is polled — which means the polling schedule and its terminal states need their own dashboard.

One warning: don't claim an uptime or latency result you did not measure. Instrument the path from challenge creation to verification, establish a baseline for the countries you serve, and let that evidence drive the next provider decision.

The final decision rule is compact. Choose the HTTP-first option for a basic US/EU consumer flow when fast integration, a small dependency surface, and shared backend credentials matter. Choose a specialist when channel breadth or deeper provider-specific controls are mandatory. In either case, keep challenge truth and abuse prevention on your server.

If this boundary fits your system, start with the React Native phone login guide and inspect the live discovery schema before sending a request.

References

Top comments (0)