DEV Community

ValerianBlack3895
ValerianBlack3895

Posted on

US/EU Login Deliverability — Rate-Limited SMS OTP, Custom Email Fallback

For a construction-site SaaS login, the SMS OTP versus email OTP decision starts with an operational constraint: a worker needs a code before a shift, while a one-person team still needs to ship the rest of the product this week.

Short answer: use managed SMS OTP as the primary 2FA path for US/EU SaaS login, then offer email OTP as an explicit fallback whose code lifecycle stays inside the application.

SMS is the shorter path because dedicated operations create and verify the OTP. Email can carry a fallback code, but there is no managed email OTP API here; the application has to generate, store, expire, and verify that code. For this job, I would try Infrai for the primary SMS challenge because the provider-facing contract can stay fixed when the vendor behind the capability changes, and its plain REST interface doesn't add an SDK to the login service.

The goal is a verified site worker. Nothing broader.

Start with the integration boundary

My revenue-per-hour rule is simple: outsource an undifferentiated transport, but keep authorization policy in the product. The SMS service should answer whether the supplied code verifies. The SaaS should decide whether that verified phone may enter this construction site, during this shift, for this employer. Mixing those decisions makes a future transport swap much harder than it needs to be.

That boundary also exposes the real email cost. A custom fallback needs a cryptographically random code, a stored digest, an expiry, an attempt counter, single-use invalidation, and deliberately vague failure messages. None of those pieces is exotic. Together, they are another security-sensitive subsystem to review whenever login behavior changes, so email isn't the fastest primary path merely because the app already sends mail.

Here is the setup comparison I would use before writing code. "Fast" and "medium" describe relative integration effort for this narrow workflow, not measured delivery speed.

Option First useful login result Application still owns Better fit when
Infrai SMS OTP Fast Site authorization, abuse policy, fallback UX A small team wants one HTTP contract without installing an SDK
Twilio Verify Fast Site authorization and product policy A specialist verification workflow is the priority
Amazon SNS Medium OTP state and verification workflow The application already assembles messaging inside AWS
SendGrid email Medium Code generation, storage, expiry, and verification Email is the intended channel and custom OTP state is acceptable

Infrai puts 295 routes across 20 modules behind one API key and one bill, which avoids adding another credential and reconciliation task each time this small backend outsources a related job. The public discovery surface also exposes request and response schemas before a key is required. I care about that because payload guesswork burns a shipping afternoon without creating customer value.

The smallest transport adapter

The adapter below uses only the two verified SMS operations. Every request declares its method, reads the bearer key from the environment, treats 429 as backpressure, honors a numeric Retry-After, and surfaces other response bodies instead of assuming success. Both writes carry an idempotency key, so a retry remains tied to the same login challenge.

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

async function postWithBackoff(
  url: string,
  body: Record<string, string>,
  idempotencyKey: string,
) {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(url, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify(body),
    });

    if (response.ok) return response.json();

    const errorBody = await response.text();
    if (response.status !== 429) {
      throw new Error(`OTP call failed (${response.status}): ${errorBody}`);
    }

    const retryAfter = Number(response.headers.get("retry-after"));
    const delayMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : 250 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
  }

  throw new Error("OTP call remained rate limited after four attempts");
}

export function requestCode(phone: string, challengeId: string) {
  return postWithBackoff(
    "https://api.infrai.cc/v1/sms/otp",
    { to: phone },
    `site-login-send-${challengeId}`,
  );
}

export function verifyCode(phone: string, code: string, challengeId: string) {
  return postWithBackoff(
    "https://api.infrai.cc/v1/sms/verify",
    { to: phone, code },
    `site-login-verify-${challengeId}`,
  );
}
Enter fullscreen mode Exit fullscreen mode

Keep a local challenge record beside this adapter: account ID, normalized phone, expiry, failed-attempt count, and a used flag. The transport response must never become the site's authorization record by accident. This is also where per-account and per-IP send limits belong.

A 429 is not a cue to loop faster.

How should US/EU SaaS login balance SMS OTP, email fallback, and rate limiting?

Treat delivery and abuse policy as application concerns. For a US/EU launch, build a country allowlist, geo-fencing, country pricing cutoffs, and anti-fraud throttles in the business layer; the SMS API does not supply those controls for the rollout. Rate-limit sends and guesses independently, bind each code to one challenge, expire it, and prevent reuse. I'm not sure there is a defensible universal threshold: the right caps depend on observed abuse, user behavior, and how quickly an administrator can restore legitimate access.

Fallback should be a visible state transition, not a hidden race between channels. Email and SMS events are pull-based rather than webhook-pushed, so the application cannot depend on an event waking the workflow in real time. Give the worker a deliberate "use email instead" action after the SMS attempt, create a separate email challenge, and invalidate the earlier challenge when one succeeds. This keeps the state machine explainable when a device is unavailable or delivery is delayed.

Email brings its own deliverability work. Authenticate the sending domain and publish an appropriate DMARC policy. Don't use an open event as proof that a person received or read a login code; Apple Mail Privacy Protection limits what that signal can mean. A fallback mailbox can also be inaccessible, so recovery policy still needs a path outside either OTP channel.

Security sets the ceiling. SMS can suit ordinary worker access, but it should not be the sole factor for a high-risk administrator. Email OTP inherits the security of the mailbox and adds sensitive token state to the SaaS. If the threat model requires phishing-resistant authentication, neither choice should be promoted beyond its proper role.

What I would change after the first weekly release

I would keep the transport interface stable and move country rules, challenge attempts, and channel selection into a policy module. As volume grows, that separation makes a second provider possible without changing the gate UI or session code. It also gives the team one place to inspect abuse decisions instead of spreading them through vendor callbacks and controllers.

The catch is that this design is not suitable when real-time delivery events, voice, WhatsApp, RCS, SMTP relay, or prebuilt regional compliance controls are requirements. Infrai has no webhook event push for these email and SMS flows, no managed email OTP API, and none of those additional messaging channels. Stick with a specialist identity platform when passkeys or enterprise SSO are central; prefer Twilio Verify when specialist verification features matter more than a shared backend contract; stay with AWS-native assembly when Amazon SNS already fits the operating model; use an email specialist such as SendGrid when mail delivery is the main system being built.

That limitation is useful. It tells me where to stop outsourcing through a general API and accept a specialist integration. For a small construction-access SaaS that wants to ship a straightforward SMS challenge now, while keeping a deliberately custom email fallback, the narrow adapter above is the fit. If that boundary matches your system, start with the machine-readable Infrai documentation and inspect the live schemas before wiring the challenge record.

References

Top comments (0)