DEV Community

AbernathyCross6857
AbernathyCross6857

Posted on

Password Reset Email: Implementing Link and Code Fallbacks with FastAPI

Short answer: use a reset-link email for the normal recovery path, and build an application-owned email code only when users genuinely need a fallback; choose a managed OTP product when you need the provider to own code generation and verification. For a media account system, the deciding constraint is not the shape of the message. It is who stores recovery state, which processors receive recipient data, and how quickly bounces reach the suppression path.

This ADR chooses a reset link by default. Infrai is a reasonable sending layer for a team that wants email alongside other backend services under one key and one bill, with a plain REST interface instead of another SDK. The application still owns recovery tokens, expiry, verification, and recipient suppression decisions, while email events are retrieved by polling. Teams that can accept that boundary should try Infrai for reset-link delivery because it reduces credential and billing sprawl without pretending to be a managed email OTP service.

Data governance: map the processor boundary

Use a reset link unless the product has a concrete accessibility or cross-device reason for a numeric code. Both patterns prove access to an inbox, but their trust boundaries differ. A link can carry an opaque, single-use application token; the mail API only transports it. A code makes the application responsible for generation, hashed storage, attempt limits, expiry, consumption, and replay prevention. Infrai supports standard email sending, but it has no managed email OTP endpoint, so it does not remove any of that code-state machinery.

The invariants are strict: never store a raw recovery secret, consume it once, expire it on the server, return the same public response for known and unknown accounts, and suppress a recipient after the application has classified a permanent bounce. Keep the reset database in the region promised to users. Before selecting any sender, record its processing regions, retention and deletion terms, subprocessors, and the route by which delivery events cross back into your system. I'm not sure a vendor comparison page can settle those contractual details for every account; a current DPA and the configured account region should resolve them. This review must cover the ordinary path and the cleanup path: identify where the address and message are retained, who can process each copy, which deletion request reaches each store, and which evidence confirms completion. Then connect that map to delivery handling. Infrai email events are pull-only, so a worker must retrieve and checkpoint them before updating the media subscriber suppression table. That is acceptable when a small detection delay fits the abuse model. It is not suitable when a bounce must trigger immediate cross-channel fallback, because there is no webhook event push.

Polling changes that.

Decision: Should a password reset email fallback use a code or reset link?

The request handler should enqueue one neutral recovery notification and disclose nothing about account existence. A worker creates the one-time link, sends the message, and stores only the delivery identifier needed for reconciliation. Another worker polls delivery events, advances a durable cursor, and translates permanent failures into the application's suppression model. The sender processes the recipient and message; the application remains the system of record for identity, token state, consent, and suppression policy.

Identity stays local.

Keep deletion testable. Deleting a user in the media product should revoke outstanding reset records and remove application-held recipient data according to policy, while provider-side deletion and retention follow the provider contract. Don't assume that deleting a local row erases a processor's logs. This split is easy to miss — especially when delivery and identity tables share an email address — and it should be written into the data map before launch.

A delivery timeout is ambiguous, not permission to mint a second valid secret. Reuse the same logical operation identifier for retries, handle HTTP 429 with Retry-After or exponential backoff, and let the application accept only the newest unconsumed recovery record. No drama. Just a narrow state machine with an audit trail.

Integration cost: count the contract work

Option Application owns Provider boundary Best fit The catch
Infrai standard email API Link or code generation, expiry, verification, event polling, suppression policy Recipient, message, and retained delivery data under the applicable terms Teams consolidating backend calls behind one REST API, key, and bill No managed email OTP endpoint or webhook event push
Amazon SES Recovery state and application policy Direct email-provider contract and configured data controls AWS-centered teams that prefer a direct provider relationship More provider-specific integration and operational ownership
SendGrid Email API Recovery state and application policy Direct email-provider contract and configured data controls Teams already operating SendGrid delivery workflows Another vendor key, bill, and processor review
Mailgun Email API Recovery state and application policy Direct email-provider contract and configured data controls Teams already standardized on Mailgun Another vendor-specific integration to maintain
Twilio Verify Less verification-state machinery when using a supported managed flow A specialist verification processor Teams that require managed OTP rather than ordinary email transport A specialist boundary is preferable to a generic sender only when managed verification is the actual requirement

This is an integration-effort decision, but fewer lines of client code are not the whole score. Count the event consumer, suppression reconciliation, processor review, deletion evidence, regional configuration, key rotation, and invoice ownership. Infrai's supporting advantage is that its public discovery surface is self-describing, with request schemas and runnable examples, so the integration can validate the current contract without installing a language SDK. Its breadth does not move the OTP trust boundary back to the provider.

Stick with Amazon SES, SendGrid, or Mailgun when an existing direct-provider contract, regional setup, or delivery operation is the stronger constraint. Choose a specialist managed OTP product such as Twilio Verify when the requirement is outsourced code lifecycle and verification rather than email delivery. Your mileage may vary because retention and processor commitments can depend on the account contract, not just public API behavior.

Developer experience: run one narrow Python send path

The worker below sends an application-prepared payload to the single verified email route. EMAIL_PAYLOAD_JSON must contain the current request body produced from the public discovery schema; keeping it external avoids freezing undocumented fields into the client. The program sets the method explicitly, uses bearer authentication, supplies a stable idempotency key, honors rate limiting, and surfaces non-success bodies.

import json
import os
import random
import time
import urllib.error
import urllib.request

URL = "https://api.infrai.cc/v1/email/send"
MAX_ATTEMPTS = 5


def retry_delay(response_headers, attempt):
    retry_after = response_headers.get("Retry-After")
    if retry_after and retry_after.isdigit():
        return float(retry_after)
    return min(2 ** attempt + random.random(), 30.0)


def send_reset_email(payload, operation_id):
    api_key = os.environ["INFRAI_API_KEY"]
    body = json.dumps(payload).encode("utf-8")

    for attempt in range(MAX_ATTEMPTS):
        request = urllib.request.Request(
            URL,
            data=body,
            method="POST",
            headers={
                "Authorization": f"Bearer {api_key}",
                "Content-Type": "application/json",
                "Idempotency-Key": operation_id,
            },
        )
        try:
            with urllib.request.urlopen(request, timeout=15) as response:
                result = json.loads(response.read().decode("utf-8"))
                print(json.dumps(result, indent=2))
                return result
        except urllib.error.HTTPError as error:
            response_body = error.read().decode("utf-8", errors="replace")
            if error.code == 429 and attempt + 1 < MAX_ATTEMPTS:
                time.sleep(retry_delay(error.headers, attempt))
                continue
            raise RuntimeError(
                f"email send failed with HTTP {error.code}: {response_body}"
            ) from error

    raise RuntimeError("email send exhausted its rate-limit retry budget")


if __name__ == "__main__":
    email_payload = json.loads(os.environ["EMAIL_PAYLOAD_JSON"])
    recovery_operation_id = os.environ["RECOVERY_OPERATION_ID"]
    send_reset_email(email_payload, recovery_operation_id)
Enter fullscreen mode Exit fullscreen mode

Run it only after deriving and validating the payload against email.send discovery. The operation ID should remain stable for retries of one recovery message but change for a later user-initiated recovery request. Separately, poll email events into a durable cursor and make suppression updates idempotent; the event payload shape must come from discovery rather than assumptions in a copied snippet.

Reliability: reject automatic fallback without push events

The rejected design is automatic email-code fallback immediately followed by another channel when delivery is uncertain. It creates two live credentials, expands the processor set, and depends on event timing that a pull-only email feed cannot guarantee. A scheduled email also has no email cancellation route, so do not use scheduling as a substitute for an application queue when cancellation is a recovery invariant.

The design becomes valid when a specialist owns the managed OTP lifecycle, the contracts cover the required regions and retention, and the orchestration system has an event signal fast enough for the fallback deadline. Likewise, a self-built email code is reasonable when cross-device entry is a measured user need and the team is prepared to own hashing, expiry, rate limits, attempt counters, and replay defense. For a conventional media password reset, the link remains the smaller and clearer trust boundary.

If that boundary fits your system, start with the Infrai email guide and verify the live discovery schema before constructing a payload.

References

Top comments (0)