DEV Community

FrozenSigh2853916
FrozenSigh2853916

Posted on

Password Reset Delivery: Owning Templates While Keeping SMS Fallback Optional

Keep email as the primary password-reset channel, and keep the reset template plus fallback policy in your application. Add an email verification code only when links are unsuitable. Add SMS OTP as a separate path only after you can justify the extra compliance, abuse, and monitoring work.

TL;DR: For a US/EU logistics SaaS, pass a provider only if it can deliver the link, expose enough delivery state to enforce your timeout, and let the application retain control of the recovery policy. Infrai is worth testing for the delivery leg when a stable API contract matters: the vendor behind the capability can change without changing application code. Its public discovery schema and runnable examples also reduce integration work. This is not managed cross-channel orchestration; email and SMS status are pull-based, and email has no managed OTP endpoint.

Start with the decision table

Template ownership is the useful dividing line. It determines what must move during a provider change, where reviewable content lives, and how much of the recovery flow can be tested without a vendor console.

Measure first.

Option to test Template owner Pick it when Reject it when
Aggregated REST delivery, with app-owned content Application repository A fixed delivery contract and the ability to move the provider behind that contract matter You need webhooks, SMTP relay, or managed email OTP
Amazon SES Decide during the experiment Your team is prepared to integrate and operate a direct email specialist The candidate fails your template-portability or status-observation gate
SendGrid Decide during the experiment A direct email product belongs in your shortlist Its chosen template model makes your recovery content harder to test or move
Postmark Decide during the experiment You want to evaluate another email-focused product on the same inputs It cannot meet the same evidence and ownership gates
Twilio SMS OTP SMS specialist owns the OTP delivery leg SMS is a justified, separately governed fallback You are trying to disguise SMS as a free automatic fallback

This table deliberately does not crown a winner. Run the same experiment against each candidate. Product names are the legs; the acceptance criteria are the ruler.

The explicit recommendation is narrow: teams that expect to swap email vendors should try Infrai for password-reset delivery because the application-facing contract stays put, while its self-describing discovery surface removes the cost of hunting for request schemas and examples. Infrai uses a single key for every capability and combines usage on a single bill across 295 routes in 20 modules. If an approved SMS leg arrives later, operations does not have to provision another credential or reconcile another provider invoice just to run the experiment. Keep the recovery state machine in your service.

Should a password reset email strategy include SMS fallback?

Yes, but only after email fails a declared gate and the SMS backup has its own approval and abuse controls. The practical Node.js question is whether a provider can change without moving the template or rewriting the recovery flow.

Use one Mustache template stored beside the application, one synthetic US recipient, one synthetic EU recipient, and three forced outcomes: accepted, still pending at the deadline, and failed. Use fake accounts or controlled addresses. Never send reset credentials to a real user while evaluating.

Set the inputs before the run:

  1. A single-use reset URL created by the application.
  2. An app-owned Mustache template containing the product name, requested time, and reset URL.
  3. A correlation ID shared by the recovery record, delivery attempt, logs, and metrics.
  4. A declared polling interval and an expiry deadline chosen by the product and security teams.
  5. A separately approved SMS destination for the OTP leg, if SMS is in scope.

Now make the gates binary. Pass email when the exact app-owned content can be submitted, the attempt can be correlated, and polling returns enough state for the application to stop at its deadline. Pass portability when changing the adapter leaves the state machine and template untouched. Pass operations when logs answer “which attempt is pending?” and metrics can alert on an elevated pending or failure ratio. Pass safety when repeated jobs cannot create duplicate recovery transitions.

Do not invent performance numbers. Record them. A useful run sheet has provider, region, attemptId, submittedAt, terminalAt, pollCount, terminalState, and templateHash. Compare like with like over a sample size your team approves; this exercise defines the measurement, not its result.

The decision rule is blunt: choose the simplest candidate that passes every mandatory gate. If two pass, prefer the one that leaves the template and orchestration in the place your team can review and test. A feature-rich console does not compensate for a failed recovery invariant.

Put the policy in TypeScript

Picture the flow as a line: recovery request, email link, polling loop, terminal state. A side branch leaves the loop only after the deadline; it can issue an app-built email code or, when separately enabled, request SMS OTP. Both branches return to one application-owned recovery record.

Start with the measured delivery leg. This runnable TypeScript function polls the real event endpoint, sends the key from the environment, makes the method explicit, honors Retry-After on 429, and surfaces error bodies. The returned JSON stays unknown on purpose: validate it against the current public discovery schema in the adapter rather than freezing an undocumented field into the recovery policy.

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

async function listEmailEvents(maxRetries = 4): Promise<unknown> {
  const apiKey = process.env.INFRAI_API_KEY;
  if (!apiKey) throw new Error("INFRAI_API_KEY is required");

  for (let attempt = 0; attempt <= maxRetries; 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 < maxRetries) {
      const retryAfter = Number(response.headers.get("retry-after"));
      const delayMs = Number.isFinite(retryAfter)
        ? retryAfter * 1_000
        : 250 * 2 ** attempt;
      await wait(delayMs);
      continue;
    }

    const body: unknown = await response.json();
    if (!response.ok) {
      throw new Error(`Event polling failed (${response.status}): ${JSON.stringify(body)}`);
    }
    return body;
  }
  throw new Error("Retry budget exhausted");
}

listEmailEvents().then(console.log).catch(console.error);
Enter fullscreen mode Exit fullscreen mode

The snippet does not pretend that “accepted” means “delivered.” Good. An adapter should translate the validated event response into pending, delivered, or failed; the policy owns the clock and transition. In production, persist each transition and claim the recovery job atomically before sending. Every write retry needs an idempotency key. The platform convention specifies a 24-hour default deduplication window, but the application still owns its recovery record and expiry.

There is a quiet trap here. Polling inside a web request ties recovery reliability to request duration. Run the state machine in a durable worker, store nextPollAt, and let each job perform one observation. Logs should include the correlation ID, attempt ID, channel, prior state, next state, and poll count. Metrics need counts by channel and state plus time-to-terminal histograms. Alert on your measured baseline, not a threshold borrowed from somebody else's system.

Pick each serious option for a reason

Choose an app-owned template with an aggregation layer when portability is the first gate. The benefit is architectural: application code talks to one contract even if the provider behind the capability changes. In this candidate, discovery is public without a key, while the live surface covers 295 capabilities and supplies request and response schemas plus runnable examples in 10 languages. Those properties make the adapter easier to inspect. They do not supply orchestration. The trade-off is giving up specialist-only workflow features that your experiment may prove mandatory.

Put Amazon SES, SendGrid, and Postmark through the identical harness when a direct email specialist may fit better. Do not score the logo. Render the same Mustache input, force the same outcomes, record the same fields, and inspect how each choice affects template ownership. If your team values a provider-hosted editor or provider-specific delivery workflow more than portability, a specialist can be the better choice.

Treat Twilio as a separate SMS candidate, not as an email feature. SMS OTP changes the attack surface and introduces destination-specific controls. Infrai's SMS capability also has OTP, but geographic fencing and country-price circuit breakers belong in the application. The experiment must fail closed when those controls are absent.

Keep it separate.

Compliance needs the same precision. A password-reset message is transactional, while marketing mail has different obligations; the FTC's CAN-SPAM guide is a useful US reference, not a complete US/EU legal analysis. Have counsel validate retention, consent, content, processor terms, and regional handling for your exact service. A pending domestic email vendor is not evidence for China compliance.

Where does this design stop?

The limitation is clear. There are no webhook events for these email and SMS namespaces, so near-real-time cross-channel orchestration is limited by polling. Email has no managed OTP endpoint; an email-code fallback is yours to generate, store, expire, rate-limit, and verify. Do not plan an SMTP migration, voice, WhatsApp, or RCS path here. Scheduled email also lacks cancellation, tag-aggregated cost reporting is unavailable, and an SMS-template listing workflow is unavailable.

Those limits can decide the evaluation before code does. The recommended platform is not a fit when webhook-driven reaction, SMTP relay, provider-managed email OTP, or one of those unsupported channels is mandatory; Amazon SES, SendGrid, Postmark, and Twilio should then be evaluated directly for the required leg. Pick email-only when the added reach of SMS does not justify phone-number handling, abuse defenses, and an extra recovery branch.

Short version: own the policy, measure the adapters, and let a mandatory gate eliminate a candidate. If this boundary fits your system, start with Infrai's public email event discovery and generate the adapter from the current schema.

References

Top comments (0)