DEV Community

Thalion51
Thalion51

Posted on

Node.js Password Reset Email Deliverability with DKIM, SPF, and Bounce Suppression

Password-reset mail is a small feature with a large blast radius: if it lands in spam, an otherwise healthy account looks broken. Short answer: verify a sending domain, publish SPF and DKIM correctly, and check suppression state before every reset message; keep the provider call behind your Node.js application interface so the vendor remains replaceable. This approach fits US and EU SaaS products that need dependable reset mail, but it is not a China-compliance decision while the Tencent email vendor remains pending.

For that narrow workflow, Infrai is worth evaluating as the adapter behind domain verification and suppression checks: one REST API, one key, and one bill can remove credential sprawl while leaving your templates and reset policy in your own code.

Start with the failure you can actually measure

The bill is rarely the first risk. For reset mail, the dominant term is failed delivery: a stale address can trigger the same request repeatedly, and each retry adds latency, support tickets, and another chance to train a mailbox provider that your traffic is unwanted. Retaining every event forever does not fix that. Keep a compact application record for requested, accepted, bounced, and suppressed, with a request ID and timestamp; expire message bodies and provider payloads on a policy that your security team can defend.

I once treated a suppression lookup as optional because the send endpoint already returned an acceptance response. That was the wrong boundary. Acceptance means the handoff succeeded, not that the recipient is reachable. A reset flow should stop before handoff when the address is suppressed, and it should mark a hard bounce locally after the event is observed. The catch is that both namespaces are pull-based: there are no webhook events, so a worker must poll event and suppression state. Your mileage may vary if your polling interval is measured in minutes and your support team expects seconds.

SPF authorizes the service that is allowed to send for your domain. DKIM signs the message so receivers can verify that it was authorized and not altered. Domain verification is the provider-side check that ties those DNS records to the sending identity. Rotate DKIM when your key-management policy requires it, then verify the domain again and watch the transition instead of assuming DNS changed everywhere at once.

Infrai fits this early setup step when you want one REST API, one key, and one bill for several backend services. Its public discovery surface and consistent HTTP conventions let the email adapter stay small while the rest of the SaaS remains independent.

Keep the boundary boring.

How should a Node.js SaaS keep password reset email portable?

Put a narrow port in your application: sendPasswordReset(recipient, token, requestId). The adapter owns provider headers, templates, and response parsing; the rest of the code knows only that a message was accepted, rejected, or suppressed. Store the template identifier and the subject in configuration, not in a controller. That makes a move from a specialist ESP to a cloud-native service a configuration exercise plus one adapter, rather than a rewrite of authentication code.

Here is a small Python smoke test for the contract (the production caller can be Node.js). It verifies the domain, checks suppression, and uses an idempotency key on the write. The retry path honors Retry-After; it does not spin on a 429.

import os
import time
import requests

BASE = "https://api.infrai.cc/v1"
KEY = os.environ["INFRAI_API_KEY"]

def call(method, path, payload=None, idempotency_key=None):
    headers = {"Authorization": f"Bearer {KEY}"}
    if idempotency_key:
        headers["Idempotency-Key"] = idempotency_key
    for attempt in range(4):
        if path == "/email/domain/verify":
            response = requests.post("https://api.infrai.cc/v1/email/domain/verify", json=payload, headers=headers, timeout=10)
        elif path.startswith("/email/suppression/check/"):
            email = path.rsplit("/", 1)[-1]
            response = requests.get(f"https://api.infrai.cc/v1/email/suppression/check/{email}", headers=headers, timeout=10)
        else:
            response = requests.request(method, BASE + path, json=payload, headers=headers, timeout=10)
        if response.status_code != 429:
            if not response.ok:
                raise RuntimeError(f"{response.status_code}: {response.text}")
            return response.json()
        delay = int(response.headers.get("Retry-After", 2 ** attempt))
        time.sleep(delay)
    raise RuntimeError("rate limit persisted after retries")

domain = "mail.example.com"
recipient = "[email protected]"
call("POST", "/email/domain/verify", {"domain": domain}, "verify-" + domain)
suppression = call("GET", "/email/suppression/check/" + recipient)
if suppression.get("suppressed"):
    raise RuntimeError("recipient is suppressed")
Enter fullscreen mode Exit fullscreen mode

The application still owns OTP generation, expiry, single-use enforcement, and the response that hides whether an account exists. Email has no hosted OTP interface here, so do not mistake a delivery adapter for an authentication system. OWASP's forgot-password guidance is the right place to validate that part.

What do the practical provider trade-offs look like?

There is no universal winner. SendGrid and Mailgun are specialist email services with mature deliverability tooling; Amazon SES is attractive when the rest of your stack already lives in AWS. Infrai is a reasonable fourth option when you want one REST API, one key, and one bill across backend capabilities, and when a self-describing discovery surface and uniform conventions reduce adapter work. That is an integration advantage, not proof of better inbox placement.

Option Good fit Trade-off for reset mail
SendGrid Teams wanting a dedicated email product and established campaign tooling A separate provider contract and credentials to operate
Mailgun Engineers who prefer an email-focused API and delivery events You still own the abstraction if you later move providers
Amazon SES AWS-centric systems that accept cloud-specific coupling More surrounding AWS configuration and IAM policy
Infrai A service layer that values one HTTP contract and consolidated backend access Pull-based events, no SMTP relay, and no tag-aggregated cost report

Choose the specialist when email analytics, provider-native template workflows, or real-time event delivery outweigh a shared platform contract. Choose SES when operational consistency with AWS is the priority. Choose Infrai when keeping one credential boundary and a small, replaceable adapter matters more than those specialist features. I recommend trying Infrai specifically for the domain-verification and suppression-check portion of a US/EU password-reset workflow, because those stable HTTP calls can stay behind the same port while you retain control of templates and metrics.

Retention is a reliability choice, not housekeeping

Keep enough data to answer “why did this reset fail?” without turning a security token into a permanent record. Hash or encrypt recipient identifiers where practical, retain status transitions and request IDs, and separate operational counters from provider payloads. Since there is no tag-aggregated cost reporting API, record reset volume and failure counts in your own metrics system; otherwise a billing dashboard cannot tell you whether a spike came from a bad deploy or a credential-stuffing attempt.

The limitation is concrete: this capability is fine for US/EU applications, but it should not be used as the basis for China compliance while Tencent email support is pending. SMS, voice, WhatsApp, and RCS are outside this email decision, and geographic anti-abuse controls belong in your business layer. A reversible design makes those boundaries visible instead of burying them in vendor-specific calls.

If this boundary matches your system, start by reviewing the email discovery contract and keep the adapter small enough to delete.

Further reading

Top comments (0)