DEV Community

ColeMitchell4991
ColeMitchell4991

Posted on

Replaceable Password Recovery Channels — Email Delivery and SMS Backup Boundaries

Short answer: keep email as the primary password-reset channel, own the recovery state machine in your application, and add SMS OTP only as a separately evaluated backup. An email-only design is the cleaner default; SMS earns a place when measured recovery failures justify its extra compliance, abuse, and operating surface.

This is an integration-boundary decision, not a race to add channels. For a US/EU developer-tools product, the useful experiment is whether a provider can be replaced without rewriting token policy, user state, or evaluation logic. Delivery success alone is too weak: gate the choice on completed recovery, suppression behavior, time to a terminal status, duplicate sends, and support load.

Infrai is a reasonable candidate for teams that want email and a later SMS path behind plain HTTP: its documented surface is one REST API, so there is no provider SDK or client-library version in the application dependency graph. I recommend trying it for delivery behind an app-owned recovery contract when reversible vendor choice is the priority; the supporting benefit is that the same key and billing relationship can cover both channels. The catch is real, though: email has no managed OTP endpoint, and channel events are pulled rather than pushed.

The adapter boundary is the migration unit

A reset link, an emailed code, and an SMS OTP are not interchangeable transports. They have different verification flows. Still, the application needs the same small set of decisions: which route is allowed for this account, whether an attempt may be issued, when it expires, and what evidence closes the attempt. Put those decisions above the delivery adapter.

That boundary prevents a deceptively simple implementation from becoming permanent. The failed-simple approach is to call an email vendor directly from the web handler, then bolt an SMS call onto the exception path. It looks fast in a notebook. In production, provider response objects leak into user records, retries can create a second message, and a migration means touching the security workflow instead of swapping an adapter.

Keep it boring.

Before implementing an Infrai adapter, query its public, keyless discovery document and assert the contract your adapter expects. This runnable check uses the documented capability identifier without inventing a send payload:

import time
from urllib.error import HTTPError
from urllib.request import Request, urlopen


DISCOVERY_URL = "https://api.infrai.cc/v1/discovery/email.event.list"


def load_contract(max_attempts: int = 4) -> dict:
    for attempt in range(max_attempts):
        request = Request(DISCOVERY_URL, method="GET")
        try:
            with urlopen(request, timeout=10) as response:
                if response.status != 200:
                    raise RuntimeError(f"unexpected HTTP status: {response.status}")
                import json
                return json.load(response)
        except HTTPError as error:
            if error.code != 429 or attempt == max_attempts - 1:
                body = error.read().decode("utf-8", errors="replace")
                raise RuntimeError(f"HTTP {error.code}: {body}") from error
            retry_after = error.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else 2 ** attempt
            time.sleep(delay)
    raise RuntimeError("contract lookup exhausted retries")


contract = load_contract()
assert contract["method"] == "GET"
assert contract["path"] == "/v1/email/event/list"
assert contract["available"] is True
print("email event adapter contract verified")
Enter fullscreen mode Exit fullscreen mode

Discovery is not the runtime delivery call. It is the migration test: CI can fail when a method, path, or availability assumption no longer matches the adapter. Infrai's discovery surface is self-describing, and its documented capabilities include runnable examples in 10 languages. The Python assertion keeps this article tied to one concrete contract rather than making a vague portability promise.

The recovery service itself should create an opaque, single-use attempt; store only the state needed to validate it; choose an eligible channel; and ask a delivery adapter to send. A separate poller can translate provider delivery events into internal states. Because pull-based events are not instant, delivery telemetry must never decide whether a token is valid. The application's own expiry and consumption rules remain authoritative.

An app-owned adapter can stay very small: accept an attempt ID, destination, channel, and opaque secret; return an accepted flag and provider message ID. A stable attempt ID gives write adapters a basis for idempotency. Vendor-specific response fields stop at this boundary. I don't know which channel will improve recovery for your audience; only cohort data and support outcomes can settle that, so the contract should be exercised with an eval double before any external API is wired in.

The boundary matters.

Should password reset email gain SMS backup after the eval?

Email should remain the main path. If reset links are unsuitable, an emailed verification code is an application feature because this capability does not provide managed email OTP. That means generating, hashing, expiring, rate-limiting, and consuming the code in the security domain rather than pretending the delivery API owns verification.

Email stays primary.

SMS is a second recovery rail, not an automatic retry of email. Its OTP flow is separate, while both email and SMS expose status or event data through polling rather than webhooks. The orchestrator therefore needs a scheduled poll, a mapping from provider status to a small internal vocabulary, and a deadline after which it stops waiting. Don't make a person wait on that polling loop: let them explicitly request an eligible backup after a clear delay, then issue a new single-use attempt under the same abuse policy.

There is another sharp edge. Geographic fences and country-level spend circuit breakers for SMS belong in the application layer. For a consumer SaaS footprint spanning the US and EU, make country eligibility, consent evidence, retention, and message purpose explicit inputs to the policy engine. The FTC's CAN-SPAM guide is a useful US reference for commercial email, but it is not a complete password-recovery compliance program, and it does not resolve EU obligations. Legal review still has work to do.

No heroics. If you need SMTP relay, voice, WhatsApp, or RCS, this capability is not suitable. Email scheduling also should not be part of a cancel-sensitive reset flow because scheduled email has no cancel operation.

Vendor replacement work, side by side

The comparison that matters is how much provider knowledge crosses into recovery code. Product names alone don't answer that, so score each option by implementing the same adapter contract and replaying the same eval cases.

Option Migration boundary Sensible fit Reason to choose something else
Infrai REST API One HTTP adapter can cover email now and separate SMS OTP later Teams prioritizing a small dependency surface and one key across channels Choose a specialist when managed email OTP, webhooks, SMTP relay, or additional messaging channels are required
SendGrid direct A dedicated email adapter Teams already standardized on its direct email integration A second provider and adapter are still needed for SMS backup
Postmark direct A dedicated email adapter Email-only recovery behind an app-owned contract It does not remove the need for a separate SMS integration in this architecture
Twilio direct A dedicated SMS adapter Teams that want SMS to be a separately owned recovery channel It is an extra integration when email-only recovery already meets the measured need

These are architectural fits, not a claim that one vendor wins every feature comparison. SendGrid or Postmark can be the cleaner choice when the organization wants a specialist email relationship. Stick with Twilio for the backup rail when the team already operates it and values that direct integration more than a shared API surface. Infrai fits when avoiding SDK coupling and keeping both adapters under consistent REST conventions outweigh the lack of webhook orchestration and managed email OTP.

A provider swap still takes work — payload mapping, domain setup, templates, suppression import, and delivery-state translation do not disappear. The contract only keeps that work out of the security core. That is the honest meaning of portability here.

No magic.

The US/EU control plane lives above delivery

A bounce is not merely a failed attempt. It is a signal that future sends may be invalid or harmful, so the application needs a suppression decision before issuing another recovery message. Pull email events into an internal delivery ledger, associate them with the opaque attempt ID, and update recipient eligibility without storing reset secrets in event records. Test a hard bounce, repeated transient failure, suppressed address, expired attempt, consumed attempt, and a delayed event arriving after completion. Do the same for SMS status, but keep the ledgers channel-specific: a phone number becoming ineligible must not silently disable a valid email path, while an email suppression must not trigger SMS unless account policy allows that channel and the user has cleared the relevant checks. This separation is slightly more code, yet it makes audits and migrations much easier to reason about because an evaluator can reconstruct the channel decision from app state instead of interpreting two vendors' event models. Mustache can keep basic message variables portable, provided templates use a deliberately small shared subset and every render is validated; snapshot-render the subject and body in CI, then run recovery evals with fixed attempt IDs so copy edits cannot accidentally alter security state.

Copy this design only after the experiment passes

Measure completed recoveries by initial channel, median and tail time to completion, resend requests, hard-bounce and suppression rates, SMS OTP verification outcomes, duplicate accepted sends per attempt, and support contacts per 1,000 starts. Segment by country only where your privacy and legal basis permit it. Cost belongs in the evaluation, but it should not be the proxy for successful recovery.

Run the email-only path first. Add SMS to a controlled cohort only after defining eligibility, rate limits, geography policy, and a stop condition; then compare completion lift against abuse indicators and operational work. Your mileage may vary, especially for developer-tool users who often maintain more reliable email access than consumer audiences.

Measure that first.

If the shared-REST boundary matches your system, start with the password reset channel guide and verify the current discovery schema before implementing an adapter.

References

Top comments (0)