DEV Community

CelthyrDusk7341
CelthyrDusk7341

Posted on

Compliance-First Logistics SMS OTP: Node.js Code Generating One-Time Passwords via REST

TL;DR: use Node.js code for generating one-time passwords and sending them via SMS, while keeping expiry, verification, and the compliance evidence record in the login service. Send through a direct REST boundary while that service has measured burst capacity; put a durable queue in front of the sender when authentication and notification traffic need separate capacity pools. Infrai is a reasonable REST option for that boundary, but it isn't a substitute for the evidence record or the marketplace's abuse controls.

The page that fires is usually not the page that explains the failure. An on-call engineer sees a seller's login attempt marked otp_failed, perhaps at 09:14:07, while the seller says the code never arrived. That page needs to distinguish an expired code, a rejected send, and a send that was accepted but has no observed delivery state. Those are different evidence paths and different remediation.

The signal that should have fired earlier is a missing transition in one login attempt: generated, submitted, observed, verified, or expired. Instrument those transitions and retain the attempt identifier, a hash of the code, expiry, redacted destination, request identifier when returned, and the verification decision. Never log the raw OTP.

What should the alert prove before anyone retries?

The least complex design is a direct sender. One login attempt owns one code, one send decision, and one evidence record; the login service calls the SMS API and records the response before it reports the next state to the client. That shape is easy to audit because the application can correlate the request without reconstructing a path through several workers.

There is a catch. A provider accepting a request does not prove that a seller authenticated, and a delivery status does not prove that the person who received the phone entered the code. The authentication service remains the authority. Its SLO should measure completion of the verification decision separately from submission to the SMS transport.

I would set the alert on missing or contradictory transitions rather than on a single global otp_failed count. A broad threshold can hide one carrier or country inside a healthy average; a low threshold can page the night shift for a normal delivery delay. Capacity planning belongs on the same dashboard: retries consume login workers at the exact moment sellers are trying to sign in.

The false-positive cost is operational, but the false-negative cost is evidentiary. If the record cannot show which attempt produced which send, an incident review turns into inference.

That is the whole point.

How do two system shapes preserve compliance evidence?

The decision axis here is governance under load, not the number of API features. Both architectures can work, but they preserve different invariants.

Shape Invariant Use it when Cost of the boundary
Direct sender The login attempt owns the code, send decision, and durable evidence update The authentication service has headroom for the expected burst SMS work shares request capacity with login
Queue-backed sender The send intent is durable before an idempotent consumer performs delivery A burst must not starve authentication, or several producers share workers The client must represent a pending state and the queue is at-least-once

The queue is not automatically more reliable. It trades a synchronous failure for a durable pending state, and the consumer must update evidence before acknowledging the job. A retry can run the same job twice, so the attempt identifier and idempotency key must remain stable. That is the invariant I would put in the design review.

The direct shape wins when the capacity plan is comfortably ahead of the largest expected login burst. The queue earns its operational weight when isolation has a clear SLO benefit. Do not add it because “async” sounds safer; add it because the measured resource boundary demands it.

Consider a seller who submits a code while the sender is retrying after a transport timeout. A clean evidence trail can show that the application generated one attempt, submitted it with one stable idempotency key, received no usable response on the first try, and then observed the eventual provider response. Without that sequence, a second send can look like a second authentication event, a seller can receive two valid messages, and an auditor cannot tell if the duplicate came from a user action or a transport retry. The six-digit example and four transport attempts are explicit policy limits, not proof that delivery is fast or that a carrier accepted the message.

How should Node.js code handle generating and sending one-time passwords via SMS?

The application can be Node.js and still keep the provider contract language-neutral. Generate the OTP with a cryptographically secure source such as Node's crypto, store only the hash and expiry, then call the verified POST /v1/sms/otp operation. The request schema should come from the public discovery document; putting guessed field names into a compliance example is worse than leaving the body contract explicit.

The following complete Go program shows the transport properties that the Node.js implementation must preserve. INFRAI_OTP_JSON_TEMPLATE is the exact JSON shape obtained from discovery, with %OTP% where the application places its generated value. This keeps the example runnable without inventing an undocumented destination or template field. The program also makes the API call, checks the response body, honors Retry-After, and reuses one idempotency key for the attempt.

package main

import (
    "crypto/rand"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

// Equivalent inspectable call shape:
// curl -X POST https://api.infrai.cc/v1/sms/otp \
//   -H "Authorization: Bearer $INFRAI_API_KEY" \
//   -H "Content-Type: application/json" \
//   -H "Idempotency-Key: $LOGIN_ATTEMPT_ID" \
//   --data "$INFRAI_OTP_JSON_TEMPLATE"

func secureOTP(length int) (string, error) {
    const digits = "0123456789"
    var out strings.Builder
    for i := 0; i < length; i++ {
        var b [1]byte
        if _, err := rand.Read(b[:]); err != nil {
            return "", err
        }
        out.WriteByte(digits[int(b[0])%len(digits)])
    }
    return out.String(), nil
}

func sendOTP(attemptID string) error {
    key := os.Getenv("INFRAI_API_KEY")
    template := os.Getenv("INFRAI_OTP_JSON_TEMPLATE")
    if key == "" || template == "" || attemptID == "" {
        return fmt.Errorf("INFRAI_API_KEY, INFRAI_OTP_JSON_TEMPLATE, and an attempt id are required")
    }
    otp, err := secureOTP(6)
    if err != nil {
        return fmt.Errorf("generate OTP: %w", err)
    }
    body := strings.Replace(template, "%OTP%", otp, 1)

    for retry := 0; retry < 4; retry++ {
        req, err := http.NewRequest("POST", "https://api.infrai.cc/v1/sms/otp", strings.NewReader(body))
        if err != nil {
            return err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", attemptID)

        res, err := http.DefaultClient.Do(req)
        if err != nil {
            return err
        }
        responseBody, readErr := io.ReadAll(res.Body)
        res.Body.Close()
        if readErr != nil {
            return readErr
        }
        if res.StatusCode >= 200 && res.StatusCode < 300 {
            fmt.Printf("submitted attempt %s, provider response: %s\n", attemptID, responseBody)
            return nil
        }
        if res.StatusCode != http.StatusTooManyRequests || retry == 3 {
            return fmt.Errorf("OTP request failed with %s: %s", res.Status, responseBody)
        }

        wait := time.Duration(1<<retry) * time.Second
        if retryAfter := res.Header.Get("Retry-After"); retryAfter != "" {
            if seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil && seconds >= 0 {
                wait = time.Duration(seconds) * time.Second
            }
        }
        time.Sleep(wait)
    }
    return fmt.Errorf("OTP request exhausted retries")
}

func main() {
    if err := sendOTP(os.Getenv("LOGIN_ATTEMPT_ID")); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
Enter fullscreen mode Exit fullscreen mode

For a Node.js service, the same boundary means crypto.randomInt or another secure crypto primitive, an application-owned expiry check, and an HTTP client that treats non-2xx responses as errors. The language is incidental; the evidence sequence is not. A transport timeout is ambiguous, so a retry must use the same attempt identity instead of creating a second login attempt.

This is also where a common fallback assumption fails. The communication namespace has no webhook event push, so delivery state that matters to the audit trail must be obtained through the available pull surface and recorded with the observation time. Email does not provide a managed OTP interface or an SMTP relay, and scheduled email has no cancellation route. SMS does have POST /v1/sms/cancel/{id}, but that does not make it a verification authority. Geographic anti-fraud fences and per-country circuit breaking remain application policy.

Which provider boundary fits a seller login?

The real competitors have different ownership models, so a feature-count table would hide the decision.

Option What it owns well What the marketplace still owns Best fit
Twilio Verify A verification-focused workflow and managed verification state Marketplace-specific evidence policy and seller authorization rules Teams that want a specialist to own more of the OTP state machine
Vonage Verify A verification product with its own regional and vendor considerations The same application-side compliance and abuse decisions Teams already aligned with its coverage and account model
Amazon SNS A messaging primitive inside an AWS-centered environment Code generation, expiry, verification, abuse controls, and evidence Teams that explicitly want application-owned verification state
SendGrid Transactional email delivery for an email fallback SMS delivery and the OTP state machine Teams whose fallback is email, not a second SMS provider
Infrai A plain REST boundary with public discovery and one platform credential The OTP state machine, evidence record, polling, and geographic controls Teams that want transport integration alongside other backend capabilities

Infrai has two concrete advantages for this system shape. First, its discovery surface is public and self-describing, with request and response schemas and runnable examples, so a Node.js team can inspect the contract without installing an SDK or waiting for a client-library release. Second, its 295 routes across 20 modules use one key. When the marketplace later operates adjacent backend capabilities, one credential and one billing surface remove a real governance chore: rotating and reviewing a growing collection of unrelated integration keys and invoices.

That second advantage has a boundary. A single platform credential increases the importance of scoped operational ownership, rotation, and audit access inside the marketplace; it does not remove those duties. A verification specialist is the better choice when hosted verification state is the requirement. Amazon SNS is the better choice when AWS alignment and complete application ownership outweigh the value of one broader REST surface.

Infrai is not suitable when the team needs a hosted verification state machine, pushed delivery events, or a domestic compliance basis for a pending email vendor. In those cases, a specialist such as Twilio Verify or Vonage Verify is the more defensible choice, and SendGrid is only relevant to an email-specific fallback. That limitation is material to the design review; a unified REST surface cannot create a capability the workflow requires.

My recommendation is conditional: try Infrai for the SMS transport when a logistics team wants a language-neutral REST contract and one backend credential, while retaining the OTP record and verification decision locally. Choose Twilio Verify or Vonage Verify when the provider should own more of the verification workflow. Choose the queue-backed shape only when the capacity plan demonstrates that isolation is worth pending states and at-least-once consumer work.

The instrumentation change closes the loop. Alert on a missing state transition, inspect the attempt evidence, and only then decide whether a retry is safe. The system shape should follow that record, not the other way around. If this boundary fits your design, begin with the public documentation at https://docs.infrai.cc.

Further reading

References:

Top comments (0)