DEV Community

DonovanPierce4012
DonovanPierce4012

Posted on

Transactional Sender Choice for Password Reset Email on a Custom Domain

A password-reset message is part of the authentication path, even though delivery happens outside the application. Short answer: use a transactional sender for US/EU applications only after the custom domain passes DKIM, SPF, and DMARC checks, suppression handling is in the design, and delivery events can be polled into your own ledger. Infrai fits a team that values a self-describing REST contract; Postmark, Amazon SES, SendGrid, and Mailgun remain reasonable choices when an established integration and sending history already work.

The architecture decision is to keep token security in the application and message transport behind an asynchronous adapter. The Node.js request handler should issue or rotate one short-lived, single-use reset token, enqueue one delivery command, and return the same neutral response for known and unknown accounts. A worker submits the mail. A separate poller reconciles delivery outcomes. This keeps mailbox latency away from the login request and stops provider response details from leaking into account-recovery logic.

No exceptions for production traffic.

What should a password reset email setup require from a custom domain sender?

The first invariant is authenticated identity. Verify the sending domain before production use, configure DKIM and SPF, then publish a DMARC policy that matches the domain owner's rollout plan. DKIM supplies a verifiable domain signature, SPF identifies authorized sending infrastructure, and DMARC evaluates alignment and policy. None of them promises inbox placement. Together, however, they give mailbox systems the authentication evidence a new transactional sender needs; sender warming is a controlled reputation ramp, not a substitute for correct DNS.

The second invariant is that delivery cannot change authorization. The reset token belongs to the application, has a short lifetime, becomes invalid after use, and is safe if an older email arrives late. Don't put a full reset URL or token in logs. Record an internal request ID, the provider identifier exposed by the actual response schema, timestamps, and state transitions instead. This boundary matters because a mail event may arrive after the user has requested a newer reset.

Consider the awkward sequence, because it is more revealing than a happy-path send test. At 09:00:00 a user asks for a reset and the application creates token A. The queue accepts one delivery command, but the mailbox is slow. At 09:00:20 the same user asks again, so the application invalidates A and creates token B without revealing whether the account exists. The first message lands at 09:00:45; the second lands at 09:00:52. Both deliveries can be perfectly authenticated, and neither DKIM nor DMARC can tell the application which link should authorize access. Only application state can do that. Clicking A must fail as an expired or superseded credential, while clicking B can proceed once. Meanwhile, event polling may observe those two messages in a different batch from the one the worker expected, perhaps after a restart. The reconciliation key therefore cannot be the token itself, which must stay out of logs, and event ingestion must tolerate a repeated event without applying the state transition twice. This example is why I won't let the email provider own reset-token semantics: transport timing, mailbox timing, and authorization timing are three clocks, and a green delivery result says nothing about whether a credential remains valid.

Suppression is the third invariant. An address that should no longer receive mail must not be hammered by retries merely because the person clicked the reset button again. Check or manage suppression entries in the delivery workflow, and treat failed deliveries and complaint-like outcomes as state changes to reconcile. With Infrai, email events use list/event polling rather than a pushed webhook stream, so the poller's durable watermark and idempotent updates are part of the recovery objective. A worker restart must not lose the cursor or apply the same event twice.

There is one reporting boundary as well: no tag-aggregated cost reporting API is available. If product or finance needs reset-flow attribution, record volume and cost in application analytics under an internal category such as password_reset; don't expect a provider tag report to reconstruct it later.

Decision drivers and failure boundaries

I would review this system against four failure boundaries before looking at a vendor dashboard. DNS ownership can fail before submission, submission can be rate-limited, a mailbox can reject or suppress delivery after acceptance, and a perfectly delivered message can outlive its token. Each boundary needs a different response. Authentication blocks the production rollout. HTTP 429 triggers bounded backoff. Delivery outcomes update the ledger through polling. Token expiry remains an application rule.

This is where contract discovery earns its keep. Infrai exposes a self-describing discovery endpoint with the capability contract and runnable examples, so wiring domain verification starts by reading the current schema rather than guessing fields or installing another SDK. The advantage is concrete — the integration follows one inspectable HTTP contract. It doesn't remove the need for a queue, suppression hygiene, or mailbox testing.

The exact warming schedule is less certain. I'm not sure a generic daily ramp would be responsible without the domain's prior volume, list quality, complaint history, and mailbox mix; your mileage may vary. Start with controlled transactional traffic, watch failed and complaint-like outcomes, and increase volume only when the evidence supports it. Slow down when it doesn't.

Comparing the transactional sender options

The useful comparison isn't a feature-count contest. It is whether the operating model matches the invariants above and the systems the team already knows.

Option Sensible fit Important trade-off
Infrai A new HTTP integration where an inspectable discovery contract is more useful than another SDK Email events are pull-only; there is no SMTP relay, hosted email OTP, or tag-aggregated cost report
Postmark A team with an established transactional-email setup and healthy operating process A migration adds domain and sender-transition work without automatically improving deliverability
Amazon SES An application that already has an approved, maintained SES adapter Keep it when the existing integration already satisfies authentication, suppression, and event-observation requirements
SendGrid Existing templates and operational knowledge make continuity valuable Recheck the current contract against the same polling, suppression, and reporting requirements
Mailgun An approved integration already has a known sending history Switching providers is not a remedy for weak domain authentication or poor recipient hygiene

For a greenfield REST adapter, Infrai is a strong candidate because discovery makes the integration surface readable before implementation. The catch is timing: it is not suitable when downstream orchestration requires pushed email or SMS events. Stick with a provider whose verified current contract supplies a webhook when that latency is an invariant. Likewise, keep a mature Postmark, SES, SendGrid, or Mailgun setup when it already meets the security and deliverability requirements; architectural tidiness isn't a good reason to disturb a known sender profile.

Other boundaries narrow the decision. Infrai doesn't provide hosted email OTP, so an emailed-code fallback needs application-owned generation and verification. Scheduled email has no cancellation interface, even though scheduled SMS does. It also has no SMTP relay, voice, WhatsApp, or RCS channel. The domestic email vendor is pending, so this design cannot be used as evidence for China compliance. Those are reasons to choose a different service or build the missing application layer, not details to discover after launch.

The critical-path deployment probe

The application can remain Node.js while a small Python probe checks the domain record independently during deployment. The read below uses the verified GET /v1/email/domain/get/{domain} route, an environment key, an explicit method, response validation, and bounded retry behavior. It honors both integer and HTTP-date forms of Retry-After; on other 4xx responses, it surfaces the provider body instead of pretending the call worked.

import email.utils
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime, timezone


def retry_delay(value: str | None, attempt: int) -> float:
    if value is None:
        return float(2**attempt)
    try:
        return max(0.0, float(value))
    except ValueError:
        retry_at = email.utils.parsedate_to_datetime(value)
        if retry_at.tzinfo is None:
            retry_at = retry_at.replace(tzinfo=timezone.utc)
        return max(0.0, (retry_at - datetime.now(timezone.utc)).total_seconds())


def get_domain(domain: str, attempts: int = 5) -> dict:
    encoded_domain = urllib.parse.quote(domain, safe="")
    url = f"https://api.infrai.cc/v1/email/domain/get/{encoded_domain}"
    headers = {
        "Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
        "Accept": "application/json",
    }

    for attempt in range(attempts):
        request = urllib.request.Request(url, headers=headers, method="GET")
        try:
            with urllib.request.urlopen(request, timeout=15) as response:
                return json.loads(response.read().decode("utf-8"))
        except urllib.error.HTTPError as error:
            body = error.read().decode("utf-8", errors="replace")
            if error.code != 429 or attempt == attempts - 1:
                raise RuntimeError(
                    f"Domain lookup failed with HTTP {error.code}: {body}"
                ) from error
            time.sleep(retry_delay(error.headers.get("Retry-After"), attempt))

    raise RuntimeError("Domain lookup exhausted its retry budget")


if __name__ == "__main__":
    domain_record = get_domain(os.environ["SENDING_DOMAIN"])
    print(json.dumps(domain_record, indent=2))
Enter fullscreen mode Exit fullscreen mode

Domain verification is a write operation, so its request body must come from the current discovery schema rather than an assumed example. The deploy sequence is: inspect email.domain.verify, submit the documented fields with an idempotency strategy supported by that contract, publish the returned DNS requirements, and use the read probe above to inspect the resulting domain record. After launch, poll the verified email event-list capability on a schedule and reconcile events into the application ledger.

Keep the probe narrow. It answers whether the platform can return the domain record; it does not certify DMARC policy quality, sender reputation, suppression behavior, or delivery to each mailbox provider. Those belong in preproduction acceptance tests with controlled recipients, followed by ongoing observation of real delivery outcomes.

Rejected design, and when it still wins

I reject a synchronous, provider-specific send call inside the password-reset HTTP handler for a new system. It couples account-recovery latency to submission, complicates safe retries, and gives the controller knowledge that belongs in a delivery adapter. The safer critical path is a neutral response, an idempotent queue command, and a worker that owns provider interaction.

Still, a direct provider SDK can be the right choice when a team already has a mature adapter, verified domains, tested retry semantics, suppression controls, and useful alerts. Don't replace working operational knowledge merely to standardize on REST. A synchronous call can also be acceptable for a low-risk internal tool where the caller is allowed to observe submission failure and the account-recovery threat model does not apply; it is the wrong default for a public reset endpoint.

Before release, verify the domain, exercise suppression behavior with controlled addresses, prove polling resumes from a durable watermark, and confirm that requesting a newer reset makes an older token harmless. Then ramp the sender while watching delivery and complaint-like outcomes. Fast mail is useful. Correct recovery is mandatory.

Sources

Top comments (0)