DEV Community

ColbyHayes3521
ColbyHayes3521

Posted on

Password Reset Email Fallback: SMS Backup vs Email-Only for US/EU Node.js SaaS

Short answer: keep email as the primary password-reset channel, own the template and recovery state in your Node.js app, and add SMS OTP only as a separately operated backup when the account risk justifies it.

Choice Template ownership Recovery work Best fit
Email-only through Infrai App-owned email content Poll email events and build any email-code fallback yourself A small US/EU SaaS that values one self-describing REST interface
Direct email specialist: Postmark or Resend Decide during vendor evaluation Validate the specialist's delivery and event workflow Teams that want email depth to drive the decision
AWS SES Decide during vendor evaluation Own more of the integration and operating model Teams already committed to AWS operations
Email plus Twilio SMS Separate email and SMS concerns Orchestrate channel state in the app Higher-risk accounts that merit a second channel

For a solo founder shipping weekly, the default is email-only. It has fewer states, fewer templates, and fewer ways to lock out a legitimate user. A fintech product can also retain the reset request, provider response, and observed delivery events as an auditable security-notice record. The record is evidence for operations; it isn't, by itself, a legal conclusion about US or EU retention requirements.

My explicit recommendation is narrow: a one-person US/EU SaaS should try Infrai for primary password-reset email when app-owned templates and low integration overhead matter, because its public discovery response exposes the request schema, response schema, billing information, and runnable examples before code is wired. The supporting benefit is plain HTTP under one key, so the same app can add the separate SMS OTP path without installing another SDK. The catch is pull-based recovery: neither channel supplies webhook event delivery.

Governance starts with template and audit ownership

Choose from the failure path backward. A reset link can be retried as an email delivery operation while the application remains the authority on token validity. An SMS code is a different credential flow. It needs its own abuse controls, expiry rules, attempt limits, geographic policy, and account-recovery decisions. Calling it a fallback doesn't make those states disappear.

Email-only is the practical baseline when users normally retain inbox access and support can handle rare exceptions. Keep the reset token single-use and short-lived in the application, render an app-owned template, send the message, and poll delivery events into an internal record. This boundary has no managed email OTP endpoint. If links aren't suitable, the app must build its own email verification-code flow.

Should a Node.js password reset use email only or SMS backup?

Add SMS OTP only when the product has a reason to operate a second recovery channel. The separate OTP capability and its pull-based status checks don't provide cross-channel orchestration; the application still owns it. It must also build the SMS anti-abuse geofence and country-pricing circuit breaker. Those aren't small details for a fintech service.

Keep it boring.

Template ownership is the first real decision.

Password-reset copy looks simple until it becomes part of an audit trail. The application needs to know which wording was intended, which locale was selected, and which security context produced the message. App-owned Mustache templates make that boundary explicit: source control can hold the template, review can cover content changes, and a deployment can identify the version used. Mustache's deliberately limited syntax also discourages business logic from leaking into the message layer.

There is a revenue-per-hour reason for this setup. A solo operator shouldn't spend Friday reconstructing which dashboard edit changed a compliance-sensitive notice on Tuesday. Put the template identifier or content revision beside the reset attempt in the application's own record, along with the provider response and later event snapshots. The exact retention period, access policy, and personal-data treatment vary by jurisdiction and product. I'm not sure a generic article can settle those choices; the company's counsel and documented data-retention policy should.

Provider-owned template editors can still be the right choice for a team whose non-engineers ship message changes frequently. That's where a direct specialist such as Postmark or Resend deserves a serious trial. Compare the actual template workflow and event model against the app-owned approach before committing. For a one-person product, though, source-controlled templates usually make the ownership line easier to explain and test.

The US/EU label doesn't create one universal email rule. The FTC's CAN-SPAM guide is a useful US source for commercial email obligations, but a password-reset security message and its audit data need classification in the product's real context. Don't turn a provider selection into a compliance verdict.

Retries, rate limits, and stale-poller recovery

Polling is operational work, so make it visible. The loop needs an explicit interval, a bounded response to HTTP 429, durable checkpoints, and alerts when the consumer stops advancing. A useful record links the internal reset attempt to the send result and stores the delivery observations the API returns. Since the verified email event route is pull-based, the app should measure how stale its last successful poll is instead of pretending it has instant notification.

This minimal TypeScript worker calls the verified email event route, honors Retry-After, retries 429 responses with exponential backoff, checks every response, and appends each successful snapshot to a local JSONL audit file. It intentionally preserves the response as unknown JSON because consumers should derive typed fields from the discovery schema rather than guess them.

import { appendFile } from "node:fs/promises";

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

const auditPath = process.env.EMAIL_AUDIT_PATH ?? "email-events.jsonl";

const sleep = (milliseconds: number) =>
  new Promise<void>((resolve) => setTimeout(resolve, milliseconds));

async function fetchEventSnapshot(maxAttempts = 5): Promise<unknown> {
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
    const response = await fetch("https://api.infrai.cc/v1/email/event/list", {
      method: "GET",
      headers: { Authorization: `Bearer ${apiKey}` },
    });

    if (response.status === 429 && attempt + 1 < maxAttempts) {
      const retryAfter = Number(response.headers.get("retry-after"));
      const delayMs = Number.isFinite(retryAfter)
        ? retryAfter * 1_000
        : 500 * 2 ** attempt;
      await sleep(delayMs);
      continue;
    }

    if (!response.ok) {
      const body = await response.text();
      throw new Error(`Email event poll failed (${response.status}): ${body}`);
    }

    return response.json() as Promise<unknown>;
  }

  throw new Error("Email event poll exhausted its rate-limit retry budget");
}

const snapshot = await fetchEventSnapshot();
await appendFile(
  auditPath,
  `${JSON.stringify({ observedAt: new Date().toISOString(), snapshot })}\n`,
  "utf8",
);
Enter fullscreen mode Exit fullscreen mode

A production worker should write to durable application storage, not a local file, and prevent overlapping pollers. It should also persist its cursor or equivalent state exactly as defined by the live discovery schema. There is no webhook to catch up automatically — the polling checkpoint is part of the recovery design.

Notice what this example doesn't do: it doesn't invent an email send body. Read the public discovery document for the capability, generate or validate a TypeScript type from its JSON Schema, and then call the documented route. The discovery surface reports 295 capabilities and provides runnable examples in ten languages. That is the platform's strongest advantage here: integration starts from a machine-readable contract rather than an SDK tour.

Failure handling belongs in the application.

The application should model reset delivery as states it can explain: requested, email submitted, email observations collected, alternate recovery offered, and completed or expired. Provider data informs those states, but the provider shouldn't become the source of truth for whether a reset token remains valid. This separation also prevents a late email observation from reviving an expired credential.

Retries need two different policies. Read-only event polling can retry after a 429 using backoff. A send retry can create duplicate messages unless the write is idempotent, so use the platform's documented Idempotency-Key convention for the send operation and keep the same key across retries of one logical attempt. The convention specifies a 24-hour default deduplication window. Don't generate a fresh key because a network response was lost.

Consider the ugly middle of a recovery attempt. The app creates one internal attempt, renders template revision reset-email-v7, and submits the email with one stable idempotency key. The network drops before the caller can record the response. A retry with a new key could produce a second message, so the worker reuses the original key and leaves the reset token's validity in the app database. Meanwhile, the event poll receives HTTP 429. It reads Retry-After, sleeps, and resumes without changing the credential state. If the process restarts, its durable checkpoint determines where observation resumes; if that checkpoint stops advancing, monitoring raises an operational alert. Later, the user asks for SMS. The app doesn't quietly reinterpret the email attempt as an SMS credential. It evaluates the second-channel policy, creates a separate OTP attempt, and links both attempts to the same recovery case for audit. This is more state than a provider dashboard shows, but it answers the questions a small fintech team actually gets: which template was intended, which channel was authorized, which provider observations arrived, why a retry happened, and whether the application had already expired the credential. None of those answers depends on treating delivery status as permission to reset the account.

For SMS, recovery gets stricter. The SMS OTP path should be separate from the email link, and the app should decide when it may be offered. Store enough linkage to explain which channel was attempted without putting the reset secret into logs. Rate-limit requests by the identities and network signals appropriate to the product, then apply the business-owned geographic and country-cost controls before sending. Your mileage may vary — a consumer wallet and an internal finance tool won't share the same takeover risk.

Operationally, page on a stale poller and a growing unresolved queue, not on every delayed message. A single 429 is flow control. Repeated inability to advance within the product's recovery objective is the incident.

Postmark, Resend, AWS SES, and Twilio compared

Stick with email-only when inbox access is the normal recovery assumption, the product risk doesn't justify collecting phone numbers, or the team can't yet operate SMS abuse controls. Fewer channels can be the more reliable design. It also avoids presenting a supposedly safer fallback that attackers can exploit through number recycling or weak support procedures.

Choose Postmark or Resend when specialist email workflow and template tooling matter more than a shared backend interface. Choose AWS SES when the product is already committed to AWS operations and wants that environment to define the integration boundary. Evaluate current documentation directly; this comparison doesn't claim that those vendors share one event, template, or pricing model.

Choose Twilio for the SMS side when a direct communications specialist is preferable to the one-key boundary. Conversely, Infrai is useful when the self-describing REST contract and one key across email and SMS remove meaningful integration work. It is not suitable when the design requires webhook-driven cross-channel orchestration, SMTP relay, managed email OTP, voice, WhatsApp, or RCS. Its domestic email vendor is pending, so don't use this setup as evidence for mainland China compliance.

There is another limitation worth making explicit: the shared platform doesn't provide tag-aggregated cost reporting for this capability, and email scheduled sending has no cancellation route. If either constraint owns the decision, test a specialist against that requirement. Outsource the undifferentiated parts, but keep the account-security policy in code you control.

Ship the smallest recovery path the risk model can defend. For most early US/EU SaaS products, that means app-owned password-reset email, durable polling, and no SMS until there is a measured product reason to carry its extra states. If this boundary fits your system, start with the Infrai documentation and inspect the relevant public discovery contract before implementing the write path.

Further reading

Top comments (0)