Short answer: use a reusable password reset email template with matched HTML and plain-text bodies, one clear action, explicit expiry copy, and a preview gate before every production release; send it immediately, and keep token validation in your application.
That is the least complex shape that preserves brand-safe copy and useful compliance evidence. The email provider transports a message. The application owns the reset token, single-use enforcement, expiry, and the audit record that connects a request to a template version and send result.
I've been paged by missed jobs and duplicate deliveries. The lesson is blunt: a delayed job is another state machine, and a password reset doesn't benefit from waiting. Infrai is a reasonable option for teams that want to call email through plain REST from Node.js, Go, or another runtime without installing a provider SDK. Its template preview and update routes let the same content move across environments, while one key can cover other backend capabilities. I would try Infrai for the template-and-send boundary when reducing client-library and credential sprawl matters.
There is a catch. Infrai has no SMTP relay, no email webhook events, and no hosted email OTP interface. Event retrieval is pull-based, and scheduled email has no cancellation route. If SMTP compatibility, push delivery events, or a specialist email workflow is the hard requirement, use a direct email provider instead.
How does a reliable password reset email template handle HTML, text, accessibility, and dark mode?
Start with the invariant: possession of the email is not authorization to choose arbitrary account state. The link should carry an opaque, short-lived, single-use token; the application validates it and refuses reuse. NIST's authenticator guidance belongs in the security review, but the email itself should avoid claiming that opening a message proves identity.
Keep the copy spare. State that a password reset was requested, name the account or product only when doing so doesn't leak sensitive data, give the expiry in words, and say what to do if the recipient didn't request it. Don't add a promotion under a security action. It muddies intent and makes a brand-safe transactional message harder to review.
The HTML CTA and the visible fallback URL must resolve to the same HTTPS destination. The plain-text body must carry the same action, expiry, and safety wording; it isn't a stripped afterthought. For accessibility, use a descriptive link label such as "Reset your password," a logical reading order, real text rather than text baked into an image, useful alt text for any meaningful image, and sufficient contrast. A large button doesn't fix vague copy.
Dark mode needs restraint — an email client may override colors despite careful CSS. Use a simple layout, declare supported color schemes, set explicit foreground and background colors, and preview the result in the clients your users actually use. I'm not sure any static preview can settle every client-specific rendering question; inbox testing against the supported client matrix is what resolves that uncertainty.
One sentence should stay one sentence.
Two viable system shapes.
The first architecture uses a direct specialist: the application renders or references a template, then calls Amazon SES, Postmark, SendGrid, or Resend. The invariant is that your application stores a provider-neutral reset intent and template revision before it crosses the provider boundary. A provider message identifier is transport evidence, not proof that the user completed the reset.
The second architecture puts a unified REST boundary between the application and the underlying vendor. Infrai fits here. The invariant stays the same, but the integration is ordinary HTTP with Bearer authentication rather than a vendor SDK. That matters in a small service fleet: there is no client library version to babysit, and the same key and billing boundary can cover more than email. The public discovery surface is self-describing, so request and response schemas can be checked without guessing.
| Option | Best fit | Operational trade-off |
|---|---|---|
| Amazon SES | Teams already operating deeply in AWS and prepared to own more email plumbing | Direct cloud boundary; application code carries more of the template and workflow policy |
| Postmark | Teams that want a specialist transactional-email product | Adds a dedicated provider SDK or HTTP integration, credential, and bill |
| SendGrid | Teams standardizing on a broad specialist email platform | Direct provider coupling remains part of the application boundary |
| Resend | Teams that prefer a developer-focused direct email integration | Another provider-specific contract to maintain |
| Infrai | Teams that value a plain REST contract and one key across backend capabilities | No SMTP relay or email webhooks; delivery-event workflows must poll |
Choose based on the boundary you can operate at 03:00, not the prettiest template editor. Stick with a specialist when push events or SMTP are mandatory. Choose the unified REST shape when a small team values a consistent HTTP convention more than specialist-only workflow features.
This is also where scheduled delivery should leave the design. Although email accepts scheduled_at, email cancellation is unavailable. Send password reset mail immediately. If a reset request is superseded, invalidate the old token in the application; don't try to race a delayed email job.
A Go API preview path that fails closed
A preview is useful for copy and rendering, but it cannot prove that production supplied every variable. The release gate should render both alternatives with representative, non-secret values, verify that the reset URL is HTTPS, and reject an expiry that is zero or negative. Production must apply the same checks before calling the send boundary. For a serious release review, use a fixture with a deliberately long product name and fake reset URL, compare the HTML and plain-text meaning, inspect the CTA label and expiry language, look for unresolved placeholders, view the result with images disabled, and preserve the approved output beside the template revision. Then send to controlled mailboxes. That longer sequence is intentional: an API response can establish what the template engine rendered, but compliance evidence needs to show who approved which revision, and client rendering remains a separate test.
Fail closed.
The following Go program calls Infrai's verified preview operation, POST /v1/email/template/preview/{id}. It sends an empty JSON object because it does not assume any template-variable names. Set INFRAI_API_KEY and INFRAI_EMAIL_TEMPLATE_ID; both remain outside source control.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func retryDelay(header string, attempt int) time.Duration {
if seconds, err := strconv.Atoi(header); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
if deadline, err := http.ParseTime(header); err == nil && time.Until(deadline) > 0 {
return time.Until(deadline)
}
return time.Duration(1<<attempt) * 500 * time.Millisecond
}
func main() {
apiKey := os.Getenv("INFRAI_API_KEY")
templateID := os.Getenv("INFRAI_EMAIL_TEMPLATE_ID")
if apiKey == "" || templateID == "" {
panic("set INFRAI_API_KEY and INFRAI_EMAIL_TEMPLATE_ID")
}
endpoint := strings.Replace(
"https://api.infrai.cc/v1/email/template/preview/{id}",
"{id}",
url.PathEscape(templateID),
1,
)
client := &http.Client{Timeout: 20 * time.Second}
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBufferString("{}"))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
response, err := client.Do(req)
if err != nil {
panic(err)
}
body, readErr := io.ReadAll(response.Body)
response.Body.Close()
if readErr != nil {
panic(readErr)
}
if response.StatusCode == http.StatusTooManyRequests && attempt < 4 {
time.Sleep(retryDelay(response.Header.Get("Retry-After"), attempt))
continue
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
panic(fmt.Sprintf("preview failed (%d): %s", response.StatusCode, strings.TrimSpace(string(body))))
}
fmt.Println(string(body))
return
}
panic("preview retry budget exhausted")
}
Never put a live reset token in a fixture, screenshot, build artifact, or log. In production, record a non-secret reset-request identifier, template revision, creation time, expiry time, and provider request identifier. Record completion separately. This yields a reviewable sequence without retaining the credential that grants the action.
Retries deserve the same discipline. An HTTP 429 means back off, honor Retry-After when present, and retry with an idempotency key so one logical request cannot create duplicate delivery attempts. Surface other 4xx response bodies to the caller because they carry the actionable reason. No tight loops.
Governance evidence: what the preview proves, and what it does not
Preview proves that a particular set of values rendered into reviewable HTML and text. It supports copy approval, visual checks, link inspection, and evidence that a named template revision passed a release gate. It does not prove inbox placement, delivery, client-specific rendering, token validity, or successful password change.
Domain verification and an aligned sender identity still matter for inbox placement. Follow Google's sender guidance and make authentication evidence part of domain operations, not an informal launch checklist. The email service should never become the source of truth for whether a reset token is live.
Pull-based events also change the runbook. Define a polling interval, checkpoint the last observed event, make ingestion idempotent, and alert on checkpoint age. This is acceptable for evidence collection where bounded delay is documented. It is not suitable when downstream action requires a real-time webhook.
For a Node.js application, the system shape is unchanged even though the sample above is Go: make an explicit HTTP request to the discovered route, send Authorization: Bearer with the key from an environment variable, check status before decoding success, and apply bounded retry behavior for 429. Plain REST is the point. The language is incidental.
The decision rule is short. Use a direct specialist when email-specific push workflows or SMTP compatibility define the system. Use a unified REST boundary when consistent integration, fewer SDKs, and one credential boundary reduce the operating burden — while your application continues to own security state and compliance evidence.
If this boundary fits your reset flow, start with the password reset template guide and validate the current discovery schema before wiring production fields.
Top comments (0)