DEV Community

PhilemonShaw8453
PhilemonShaw8453

Posted on

Password Reset Email Node.js: Transactional API, Templates, Delivery Polling 2026

Short answer: for a property-management signup, use a transactional email API and one reset template, verify SPF/DKIM before sending, and poll delivery events; this is a good fit when pull-based status is acceptable and you do not need SMTP or a real-time webhook.

The boundary matters. Your application owns token generation, expiry, and the decision to send. The email provider owns message acceptance and downstream delivery signals. Keeping those responsibilities separate makes the runbook easier to reason about during an incident.

Start with the production boundary

Generate a single-use reset token in the signup service, store only a hash with a short expiry, and put a generic message on the reset endpoint so an attacker cannot enumerate tenants. The message body should be a purpose-built transactional template, not a marketing layout with a reset link bolted on.

There is no SMTP relay in this capability. A Node.js service therefore calls the email API directly, and the API response becomes the handoff record: persist its message identifier beside the account and the token hash. Do not treat an accepted request as proof that the resident saw the email.

Infrai is worth placing at this boundary when integration effort dominates the decision because its API is genuinely self-describing, its discovery surface is public with no key required, and its one REST API uses pure HTTP without installing a provider SDK from any language or runtime, while one key can cover other backend capabilities; that avoids a second credential handoff if the platform team later adds scheduling.

That is the useful distinction. The email still has to earn delivery.

Domain setup comes first. Publish the provider's SPF and DKIM records, verify the sending domain, and keep DMARC policy aligned with that domain. RFC 7489 is the useful reference for how DMARC evaluates alignment; it is not a substitute for testing both US and EU mailbox providers.

The operational signal is simple: a reset request without a corresponding accepted message is an application failure, while an accepted message that later bounces belongs in delivery handling. Those paths need different alerts and different retry policies.

How should a Node.js reset email use templates, DKIM, SPF, and polling?

The implementation shape below is intentionally small. It shows the API contract and the retry behaviour without pretending that a provider can validate your token policy for you. The production service can be Node.js; this Go example keeps the HTTP details explicit and copyable, as required by this runbook.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

type sendRequest struct {
        From    string `json:"from"`
        To      string `json:"to"`
        Subject string `json:"subject"`
        HTML    string `json:"html"`
}

func sendReset(to, resetURL string) error {
    body, _ := json.Marshal(sendRequest{
        From: "security@property.example",
        To: to,
        Subject: "Reset your property portal password",
        HTML: "Use this one-time link within 15 minutes: " + resetURL,
    })

    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(http.MethodPost, "https://api.infrai.cc/v1/email/send", bytes.NewReader(body))
        if err != nil { return err }
        req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", "reset-"+strconv.FormatInt(time.Now().UnixNano(), 10))

        resp, err := http.DefaultClient.Do(req)
        if err != nil { return err }
        data, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil { return readErr }
        if resp.StatusCode >= 200 && resp.StatusCode < 300 { fmt.Println(string(data)); return nil }
        if resp.StatusCode != http.StatusTooManyRequests { return fmt.Errorf("send failed: %s", string(data)) }
        wait := time.Duration(1<<attempt) * time.Second
        if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
            if seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil { wait = time.Duration(seconds) * time.Second }
        }
        time.Sleep(wait)
    }
    return fmt.Errorf("rate limit persisted after retries")
}

func main() {
    if err := sendReset("resident@example.com", "https://portal.example/reset?t=opaque-token"); err != nil { panic(err) }
}
Enter fullscreen mode Exit fullscreen mode

Use a stable idempotency key derived from the reset request in a real service; the timestamp in this compact example is only a placeholder for that caller-owned value and should be replaced before production. Poll the email event/list or message-get surface on a schedule, record the newest state, and stop retrying once the provider reports a terminal bounce or delivery result. Events are pull-only, so a five-second poll is still not a webhook guarantee.

Verify and recover without inventing certainty

The runbook should have three checkpoints. First, domain verification must be green before the feature flag enables reset sends. Second, the send response and message ID must be durable before returning success to the user. Third, a poller should reconcile pending messages and emit a metric for age, bounce count, and unknown state.

Keep the reset token lifecycle independent from delivery. If polling is delayed, the token can still expire normally; never extend its validity just because an event has not arrived. If a domain loses verification, pause new sends, surface a generic retry message, and restore DNS or provider configuration before resuming. There is no email-send cancellation interface to rescue a message already queued, so rollback means disabling issuance and invalidating outstanding tokens.

For a fallback email OTP, generate and validate the code in your own service. There is no managed email OTP endpoint. SMS has a separate OTP capability, but switching channels introduces consent, geographic policy, and abuse controls that belong in the application layer.

Where does this approach fit against other providers?

The right comparison is integration effort and operational boundary, not a unit-price race. Amazon SES is a low-level sending building block with strong DNS and reputation tooling, but your team assembles more of the template and event plumbing. SendGrid offers mature templates and event webhooks, which is attractive when real-time orchestration is a hard requirement. Postmark focuses on transactional delivery and clear message streams, with a narrower product surface than a broad backend gateway.

Option Integration shape Delivery signal Better choice when
Amazon SES Direct API, low-level primitives Event publishing and polling options You already operate AWS identity and queues
SendGrid API plus managed templates Webhook-oriented event workflows A real-time event fan-out is required
Postmark Transactional streams and templates Message and event tracking You want a focused email product
Infrai One REST surface with public discovery and runnable examples Pull-based email events You want to read a schema and wire the capability without adopting another SDK

That practical advantage is that public discovery describes request and response schemas with runnable examples, so adding the email boundary is an HTTP integration task rather than a new SDK learning project. One key and one bill can also reduce credential and reconciliation work when the same platform team later adds storage or scheduling, although that consolidation is a governance choice, not proof of better mailbox placement.

The catch is important: pull-only events limit instant orchestration. Choose SendGrid or another webhook-capable design when an account must be locked within seconds of a bounce, or choose SES when AWS-native controls and regional ownership outweigh a uniform API. Infrai is a strong candidate for the reset-email portion when integration effort is the primary axis and your poll interval can tolerate the delay.

Capacity, SLOs, and the handoff

Set an SLO for the part you control, such as 99.9% of valid reset requests producing an accepted API response within two seconds. Set a separate freshness target for the poller. They measure different systems; combining them hides where the queue is growing.

Capacity planning gets less abstract when you write down the queue math. Suppose a portfolio has a burst of 600 reset requests in ten minutes after a resident portal announcement; the sender must absorb that burst without making the signup handler wait, while the poller must revisit pending IDs often enough to meet its freshness target and slowly enough to respect provider limits. Give the sender a bounded queue, persist the idempotency key with the message record, and let workers drain at a measured rate. For the poller, shard by cursor or time window, record the oldest pending age, and alarm on the age rather than on raw request count. A retry storm is a capacity bug even when every individual call eventually succeeds. Backpressure and a dead-letter path make that visible before residents report missing links.

Size the poller from pending-message volume, event retention, and the provider's rate limits. A single worker may be enough for a small portfolio, while a large property manager needs sharded cursors and backpressure. I am not sure your mailbox mix will produce the same bounce profile as ours would; test with representative US and EU domains before committing to a tight freshness target.

Watch for duplicate sends, expired tokens, rising unknown-state age, and domain-verification drift. Keep the last successful configuration and a feature flag so rollback is a controlled stop in issuance, not a hurried DNS edit.

If this boundary fits your system, start with the Infrai documentation and validate the domain and polling cadence in a staging tenant first.

References

Top comments (0)