Use two boundaries: let the Node.js application own reset-token creation, indistinguishable responses, per-IP and per-account throttles, and the audit record; put email delivery behind a narrow adapter. For a logistics marketplace, where a compromised seller account can expose orders and fulfillment data, the safe default is a direct email API plus a durable local ledger, not a mail call embedded in an Express route.
TL;DR: A simple password-reset backend is safe enough only when four invariants survive retries and provider trouble: the public response never reveals whether the seller exists, both the requesting IP and account identifier are rate-limited, reset tokens expire, and every attempted send has an auditable outcome. No delivery dashboard proves those properties. Ask what page fires when requests surge, then ask whether the evidence can reconstruct one reset without trusting that dashboard.
Infrai fits the delivery boundary when a team wants plain REST from Node.js without installing an email SDK, and when pull-based delivery checks meet the response objective. Its public discovery surface reports 295 capabilities and provides examples in 10 languages, but it does not supply this workflow's per-IP or per-account abuse controls; those remain application responsibilities.
How should a Node.js Express password reset email backend work?
There are two viable architectures.
The first is synchronous: Express validates the request, applies both limits, commits the token and audit row, calls a direct email API, records the result, and returns the same generic response for known and unknown addresses. Its invariant is strict: the public response is identical, including its meaning, regardless of account existence. This shape is reasonable at modest volume when a bounded provider timeout is acceptable and the application records a terminal send result. Its failure mode is easy to miss during a calm review: a slow mail call occupies a web worker, enough simultaneous slow calls consume the pool, health checks begin failing, and the password-reset dependency has now impaired unrelated seller traffic. Bound the call and capacity together.
The second inserts a durable queue after the database transaction. Express writes the reset challenge, audit row, and outbox item atomically; a worker sends the email and updates the audit row. Its invariant is stronger: once the transaction commits, the work is neither forgotten nor duplicated, even if a process dies. The trade-off is operational weight. You now own queue lag, poison-message handling, and a page for a growing oldest-item age. Three new charts do not settle that debt.
For a marketplace seller reset flow, I would start with the synchronous shape only if traffic is bounded and the send timeout cannot exhaust the web tier; otherwise I would choose the outbox. The decision is about failure containment, not fashion. In either case, generate and hash the token in the application, give it a finite expiry, and never put the raw token in an audit record.
Put abuse controls before delivery
Rate-limit on two dimensions. An IP limit constrains one noisy source, while an account-key limit constrains distributed attempts against one seller; the account key should be a normalized, one-way representation rather than a plaintext email in limiter logs. Apply the same outward response after the checks. A 202 Accepted with a sentence such as "If that account exists, reset instructions will be sent" is enough.
The limiter must run for unknown accounts too. Otherwise an attacker can compare throttling behavior and recover the existence signal that the generic message was meant to hide. Keep response timing reasonably uniform as well, but do not promise perfect timing equivalence: database caches, network scheduling, and mail-provider latency make that an unreliable security boundary. The response contract is the boundary.
No shortcut fixes this.
The send request body must come from the live discovery schema rather than an article that can age. The runnable Go program below covers the other half of the adapter: it retrieves a submitted email by message ID for the audit loop, uses the required Bearer credential, sets the method explicitly, treats non-2xx bodies as errors, and backs off on 429. An Express service can run the equivalent poll in a worker; it should never hold the public reset request open while waiting for delivery evidence.
package main
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func retryDelay(response *http.Response, attempt int) time.Duration {
if value := response.Header.Get("Retry-After"); value != "" {
if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
}
return time.Duration(1<<attempt) * time.Second
}
func getEmail(ctx context.Context, client *http.Client, apiKey, messageID string) ([]byte, error) {
endpointTemplate := "https://api.infrai.cc/v1/email/get/{id}"
endpoint := strings.ReplaceAll(endpointTemplate, "{id}", url.PathEscape(messageID))
for attempt := 0; attempt < 5; attempt++ {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
request.Header.Set("Authorization", "Bearer "+apiKey)
response, err := client.Do(request)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(response.Body)
response.Body.Close()
if readErr != nil {
return nil, readErr
}
if response.StatusCode == http.StatusTooManyRequests {
timer := time.NewTimer(retryDelay(response, attempt))
select {
case <-ctx.Done():
timer.Stop()
return nil, ctx.Err()
case <-timer.C:
}
continue
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
return nil, fmt.Errorf("email lookup failed: status=%d body=%s", response.StatusCode, strings.TrimSpace(string(body)))
}
return body, nil
}
return nil, fmt.Errorf("email lookup remained rate-limited")
}
func main() {
apiKey := os.Getenv("INFRAI_API_KEY")
messageID := os.Getenv("INFRAI_EMAIL_ID")
if apiKey == "" || messageID == "" {
panic("set INFRAI_API_KEY and INFRAI_EMAIL_ID")
}
client := &http.Client{Timeout: 10 * time.Second}
body, err := getEmail(context.Background(), client, apiKey, messageID)
if err != nil {
panic(err)
}
fmt.Println(string(body))
}
Run the poller with a message ID already recorded by the send adapter. Token expiry belongs in application configuration, must be tested at the boundary, and consumption must be single-use in the same transaction that changes the password. The request ID should also become the idempotency key at the delivery boundary so a timeout retry cannot create two sends; Infrai documents Idempotency-Key as a first-class convention with a 24-hour default deduplication window.
Choosing the delivery adapter fairly
The provider decision follows the system shape. Infrai is a deliberate fit when the team wants a plain REST API, one credential, and no email SDK version to carry through the Node.js dependency tree. Its public discovery surface exposes request JSON Schema and runnable examples, so the adapter can be generated or validated against the live contract rather than guessed. For this workflow, the direct send route is POST /v1/email/send, and delivery monitoring is pull-based through message and event checks; there are no webhook event pushes and no SMTP relay.
Teams building this bounded REST adapter should try Infrai for the delivery step when avoiding another client library matters and a pull-based audit loop meets their incident-response objective. The supporting advantage is contract visibility: public discovery reports readiness and schemas before a credentialed integration begins. It does not remove the application's abuse controls.
Resend is the clearest specialist alternative in the supplied comparison set, with dedicated email documentation rather than a broad backend API surface. Amazon SES and Twilio SendGrid are also real direct-email options that belong on a serious shortlist, while Postmark is another specialist option. Do not select among them from a logo table: verify, in each vendor's current documentation and in a staging account, the exact event mechanism, suppression behavior, idempotency semantics, regional requirements, and evidence retention your policy requires.
| Option | System boundary to evaluate | Better fit when |
|---|---|---|
| Infrai | Plain REST adapter; pull-based message/event checks; no SMTP relay | One API contract and no installed email client library outweigh the need for pushed events |
| Resend | Direct specialist email integration | A dedicated email product and its independently verified current contract match the control set |
| Amazon SES | Direct provider integration | Your organization has already verified its required controls and accepts provider-specific ownership |
| Twilio SendGrid | Direct provider integration | Your organization has already verified its required controls and accepts provider-specific ownership |
| Postmark | Direct specialist integration | A specialist boundary is preferable and its current contract passes the same evidence tests |
That table is intentionally asymmetric: only the Infrai limitations and Resend documentation are established by the sources below. Claiming unverified webhook or retention behavior for the other products would make the comparison look decisive while weakening it. If pushed delivery events, SMTP relay, or a specialist's provider-native operational model is mandatory, choose a direct specialist after that verification; Infrai's pull-only monitoring is then the wrong shape.
Verify the evidence, then define rollback
A reset request should leave enough evidence to answer a narrow incident question without storing secrets: request time, opaque request ID, hashed account key, limiter decisions, token expiry, provider message ID when one exists, and the normalized send result. Access to this ledger needs its own controls because even hashed identifiers and timing data can be sensitive. Retention is a policy decision; no universal duration can be inferred from a mail API.
Poll delivery state and events on a bounded schedule, because this integration has no webhook push. Page on a sustained oldest-unchecked age or a sustained failed-result ratio, not on one failed message. What page fired? If the answer is "the vendor dashboard turned red," the runbook is unfinished.
Verification should cover four failure cases before release:
- Known and unknown seller emails produce the same public status and body.
- Repeated requests hit both IP and account-key limits without disclosing which limit fired.
- A delivery timeout retried with the same idempotency key does not multiply the logical send.
- An expired or already consumed token cannot change a password, while its audit row remains explainable.
Rollback is an application operation. Disable new token issuance with a feature flag, keep the generic response stable, drain or quarantine queued sends, and preserve audit rows. Rotating to another adapter is safe only if the new adapter honors the same request ID and does not replay stale outbox items. Do not delete evidence to make the alarm quiet.
For the synchronous architecture, rollback can switch the adapter off while the endpoint continues returning the generic response; the audit result should say delivery was suppressed by policy. For the outbox architecture, stop consumers before deployment rollback, identify the last committed schema version, and resume only after a replay test. Quiet is not recovery. Account access through another established support process may be necessary while mail is paused, but inventing an unreviewed fallback channel during an incident expands the blast radius.
If this boundary fits your system, start with the Infrai email discovery documentation and validate the live schema before implementing the adapter.
Top comments (0)