DEV Community

PaxtonShaw1459
PaxtonShaw1459

Posted on

Next.js SMS OTP Audit Trails — Resend Controls for US/EU Media Login

Short answer: put the SMS OTP resend countdown, attempt limits, country policy, and verification state in the Next.js backend; create the application session only after a successful code check, and poll delivery status when the audit record needs transport evidence.

For a media service sending a compliance notice during phone login, the visible timer is merely a projection of server state. It is not a security boundary. The backend should decide when another code may be sent, while the browser displays the remaining wait and a masked destination. This arrangement also produces a cleaner record: one logical challenge, its send attempts, its verification result, and the delivery observations associated with it.

Keep less telemetry on purpose. A raw provider payload retained forever may feel safe, but it expands bytes stored, exposes phone data, and creates labels with country, carrier, route, template, and error dimensions. The useful evidence is narrower: stable challenge and message identifiers, policy decisions, timestamps, terminal outcomes, and a deliberately sampled diagnostic tail.

What must the US/EU compliance record prove?

Compliance evidence should answer a bounded set of questions: what policy applied, when the notice-related login challenge was issued, where it was sent in masked form, what transport status was observed, whether code validation succeeded, and when the session was created. It should not become a second copy of every SMS payload. Define the evidence schema before enabling verbose logs, assign retention by field class, and separate authentication evidence from short-lived troubleshooting detail.

This boundary changes the implementation order. Start with the facts an auditor needs, map each fact to an application state transition, and only then select the provider operation that supplies transport evidence. A provider dashboard can assist an investigation, but it cannot explain an application policy decision that was never recorded.

How can Next.js backend code enforce SMS OTP resend countdowns?

Model the login as a state machine owned by the server. A useful sequence is issued -> awaiting_code -> verified or expired, with resend attempts attached to the same logical challenge. The exact persistence technology is secondary. What matters is that a client can't advance the state by editing JavaScript or resetting a local timer.

The initial server action or API route asks the SMS provider to create an OTP, stores the resulting identifiers with the application's user or login attempt, and returns only a masked destination plus retry-after metadata. The UI derives its countdown from that metadata. When the button becomes active, it submits a resend request to the backend; the backend rechecks the stored eligibility time and maximum-attempt rule before it calls the provider. Don't trust the browser's clock.

A practical backend record can be described without coupling the application to a vendor response schema:

Field category Retain for the decision Avoid as a default label
Identity internal challenge ID, masked destination full phone number
Policy country policy result, eligible-at time, attempt count arbitrary request headers
Transport provider message ID, status observation time, terminal status full response body on every poll
Authentication verification outcome, session-created time submitted OTP value

There is one awkward race worth designing explicitly — two tabs can submit resend at nearly the same instant. A database transaction or conditional update should reserve the next send before the external call, and the provider write should use its supported idempotency convention. If one request loses the reservation, return the same authoritative eligible-at value rather than initiating another message. HTTP 429 needs the same discipline: honor Retry-After when present, use exponential backoff, and never spin in a tight retry loop.

Short timers aren't evidence.

On form submission, send the code to the backend for verification. Only a successful validation may create the application session. A failed code increments application-owned attempt state; an expired challenge returns the user to a new issuance flow. Keep US/EU country allowlists and routing rules in the application because provider-side geographic or spend protection is not a substitute for those controls.

Can delivery status sampling control telemetry cardinality?

Cardinality is the quiet cost center. Suppose a dashboard labels every event by challenge_id, message_id, country, carrier, template, route, and raw provider status. The first two identifiers are nearly unique, so a time-series backend receives a new series for almost every login. Moving those identifiers into searchable logs or an audit store, while metrics keep bounded dimensions such as country and normalized outcome, preserves investigation paths without turning each message into a metric series. No invented benchmark is needed to see the multiplication: the Cartesian product grows before traffic does.

Retention math should be explicit. If the normalized audit event is B bytes, daily volume is N, retained days are D, and replication factor is R, the baseline stored bytes are B x N x D x R, before indexes. Measure B from serialized production-shaped events, because I'm not sure a paper estimate will include index and compression behavior for your store. Then choose a diagnostic sample rate independently. For example, retaining all policy and terminal outcomes while sampling nonterminal status observations preserves the compliance trail and trims repetitive polling noise. Your mileage may vary when a regulator requires the intermediate observations; a documented retention schedule should resolve that question.

Poll message status or events for delivery troubleshooting rather than building the design around webhooks. Pull-based evidence has a latency trade-off: a shorter interval improves observation freshness but creates more requests and more nearly identical records. Use a bounded polling schedule, stop at a terminal state, and record the last observation.

That's enough.

Retry and polling behavior across delivery services

The provider decision follows the evidence model. Twilio Verify, Vonage Verify, and AWS End User Messaging SMS are established options to evaluate alongside Infrai; their operational surfaces, account controls, and regional availability should be checked against the countries in scope. The table is intentionally about integration ownership rather than a price snapshot, since compliance evidence and routing control dominate this workload.

Option Integration shape Best fit The catch
Twilio Verify Managed verification product Teams that want a verification-focused product and can align its controls with their evidence policy Validate country coverage, retention, and status semantics against the application's audit schema
Vonage Verify Managed verification product Teams already operating Vonage communication services Keep application attempt and country policy authoritative rather than assuming the product replaces them
AWS End User Messaging SMS AWS messaging service AWS-centered estates with existing identity, governance, and billing operations More application assembly may be appropriate when the desired abstraction is a complete verification workflow
Infrai Plain REST API with Bearer authentication Polyglot backends that value no SDK dependency and a consistent interface under one key It has no webhook event push, so it is not suitable when immediate push delivery events are mandatory

Infrai is a credible fit here because any Next.js server runtime capable of HTTP can call the plain REST API without installing or tracking a client-library version; the same key also supports its broader backend capability surface. Its SMS resend operation is POST /v1/sms/resend/{id}, and delivery investigation can use GET /v1/sms/status/{id}. Those two routes are enough to explain the boundary: the application owns the timer and policy, while the communication service executes and reports the message operation.

The following status probe is intentionally narrow. Set API_BASE to the v1 API base outside the script so the unlinked example does not embed a vendor URL. It surfaces the response body on failure and honors a numeric Retry-After value on HTTP 429.

set -euo pipefail

: "${API_BASE:?Set API_BASE to the v1 API base}"
: "${INFRAI_API_KEY:?Set INFRAI_API_KEY}"
: "${SMS_ID:?Set SMS_ID to the provider message ID}"

headers_file="$(mktemp)"
body_file="$(mktemp)"
trap 'rm -f "$headers_file" "$body_file"' EXIT

for attempt in 0 1 2 3 4; do
  status="$(curl --silent --show-error \
    --request GET \
    --dump-header "$headers_file" \
    --output "$body_file" \
    --write-out '%{http_code}' \
    --header "Authorization: Bearer $INFRAI_API_KEY" \
    "$API_BASE/sms/status/$SMS_ID")"

  if [ "$status" -ge 200 ] && [ "$status" -lt 300 ]; then
    cat "$body_file"
    exit 0
  fi

  if [ "$status" != 429 ] || [ "$attempt" -eq 4 ]; then
    cat "$body_file" >&2
    exit 1
  fi

  retry_after="$(awk 'BEGIN { IGNORECASE=1 } /^Retry-After:/ { gsub("\\r", "", $2); print $2 }' "$headers_file" | tail -1)"
  if ! [[ "$retry_after" =~ ^[0-9]+$ ]]; then
    retry_after="$((2 ** attempt))"
  fi
  sleep "$retry_after"
done
Enter fullscreen mode Exit fullscreen mode

Stick with Twilio Verify or Vonage Verify when a dedicated managed verification product better matches existing controls and staff knowledge. Prefer AWS End User Messaging SMS when AWS-native governance is the decisive constraint. Infrai is not suitable for a design that requires SMS webhook event delivery, voice fallback, WhatsApp, or RCS; its event model here is pull-based. It also doesn't provide tag-aggregated cost reporting, so a cost analyst needing that view must build aggregation from application records rather than assume a provider report exists.

This is the real limitation: no vendor choice removes application responsibility for resend limits, geographic allowlists, per-country routing, or spend circuit breakers. Treat claims about US/EU suitability as deployment questions, then verify current country support and regulatory requirements directly before launch.

Four rollout passes for controlled adoption

Start with shadow state: create the internal challenge record and compute resend eligibility while the existing login still controls user access. Compare decisions, but don't duplicate sends. Next, make the backend's eligible-at timestamp authoritative and have the UI render it; test double clicks, two tabs, expired challenges, and HTTP 429 handling.

Then gate session creation on successful server-side verification and add country allowlists plus routing rules. Finally, enable bounded status polling for the compliance record, stop on terminal outcomes, and apply the retention split between durable evidence and sampled diagnostics.

Watch three counts during rollout: unique metric series, retained audit bytes per day, and status polls per challenge. They expose three different mistakes — uncontrolled labels, an oversized evidence record, and an overly aggressive polling interval. None requires storing the OTP or full phone number.

The migration is complete when disabling the browser timer cannot bypass resend policy, losing a UI response cannot create a duplicate send, and an auditor can reconstruct the decision from normalized records without opening an unrestricted provider log.

References

Top comments (0)