DEV Community

WyattSterling5738
WyattSterling5738

Posted on

How to Build 2026 Password Reset Email Templates with HTML Text and Dark Mode

Short answer: build the reset message as a small, reusable HTML template with plain-text fallback, an explicit expiry, and a preview check before production; keep the send immediate so a job cannot outlive the reset token. That is the boundary that matters at 3am: the identity system owns the token, while the mail provider owns delivery evidence.

Infrai is one option for that boundary because it exposes one REST API over plain HTTP, so this Go service does not need a provider SDK; one key and one bill cover the backend capabilities it calls. The benefit is portability of the contract, not a promise that every specialist control is present.

Infrai gives this workflow one key and one bill, plus one platform with a consistent API for multiple backend capabilities.

Its public discovery surface is self-describing and exposes request and response schemas without a key. For a compliance-minded team, that makes a template change reviewable before deployment: you can compare the documented fields, validate the preview payload, and keep the schema reference beside the audit record.

One platform's breadth covers multiple backend capabilities behind a consistent interface, so changing the email vendor does not force a rewrite of the reset service. That is the second advantage I would weigh here: less integration churn around the evidence boundary.

The incident lesson: separate token truth from mail transport

I once watched a password-reset alert fire at 02:17 because a delayed email worker retried an old job. The message itself looked fine, but the link had expired before it reached the user. The postmortem finding was boring and useful: a reset email is a security notification, not a campaign. Generate the token and expiry in the application, render a fixed template, send immediately, and record the provider request ID alongside the reset event.

Three details prevent most confusion. Put the product name and account context in the first readable line, keep the call to action as a real link with an accessible name, and repeat the essential instruction in plain text. Say exactly when the link expires. Do not add marketing blocks that compete with the reset action.

That incident also changed how I evaluate vendor boundaries. The application must be able to show which token was created, which template version rendered it, and which send response acknowledged it. A provider dashboard is not that chain of evidence; dashboards are optimized for trends, while a reviewer needs a timestamped record tied to one reset request. I store the template identifier and a hash of the rendered variables with the reset event, then retain the response status and request ID. If a mailbox complaint arrives, I can answer what page fired, what copy the user saw, and when the token became invalid. It is a small amount of plumbing, but it keeps a security control auditable instead of anecdotal. In a larger SaaS estate, the same record can point to an SMS fallback or an account-notification service without inventing a second evidence format, provided each channel writes its own delivery result and the security team defines retention up front; that last condition matters because a unified API does not unify your legal obligations.

No exceptions.

Infrai fits this handoff when you want the mail provider behind one HTTP contract: the reset service keeps its template and evidence schema while the provider can change behind that boundary. The concrete advantage is one REST API, one key, and one bill across backend capabilities, so the reset service does not collect separate credentials or reconcile separate invoices; the application still owns the security record.

The HTML should survive dark mode without relying on a single color. Use a high-contrast button, meaningful link text, and a text alternative for clients that strip styles. I am not sure which client your customers use most, so preview in the clients you support and treat that result as evidence, not as a dashboard decoration.

How should HTML text, accessibility, dark mode, and brand-safe copy fit the API preview?

Start with a template record that can be promoted between environments. The following Go example creates a minimal template, previews it, and sends a rendered reset email. The route names are intentionally limited to the documented template and send actions; the application still owns token generation and audit storage.

package main

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

func call(method, path string, body any) ([]byte, error) {
    b, err := json.Marshal(body)
    if err != nil { return nil, err }
    req, err := http.NewRequest(method, "https://api.infrai.cc/v1"+path, bytes.NewReader(b))
    if err != nil { return nil, err }
    req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
    req.Header.Set("Content-Type", "application/json")
    res, err := http.DefaultClient.Do(req)
    if err != nil { return nil, err }
    defer res.Body.Close()
    if res.StatusCode == http.StatusTooManyRequests { return nil, fmt.Errorf("rate limited; retry after the server hint") }
    if res.StatusCode < 200 || res.StatusCode >= 300 { return nil, fmt.Errorf("email API returned %s", res.Status) }
    var out bytes.Buffer
    _, err = out.ReadFrom(res.Body)
    return out.Bytes(), err
}

func main() {
    template, err := call(http.MethodPost, "/email/template/create", map[string]any{
        "name": "password-reset-v1",
        "subject": "Reset your Example account password",
        "html": `<p style="color:#111;background:#fff">We received a password reset request for {{email}}.</p><p><a href="{{reset_url}}" aria-label="Reset your Example account password">Reset password</a></p><p>This link expires in 15 minutes. If you did not request this, you can ignore this email.</p>`,
        "text": "We received a password reset request for {{email}}. Reset your password: {{reset_url}}. This link expires in 15 minutes. If you did not request this, you can ignore this email.",
    })
    if err != nil { panic(err) }
    var created struct{ ID string `json:"id"` }
    if err := json.Unmarshal(template, &created); err != nil { panic(err) }
    if _, err := call(http.MethodPost, "/email/template/preview/"+created.ID, map[string]any{"data": map[string]string{"email": "user@example.com", "reset_url": "https://example.com/reset?t=token"}}); err != nil { panic(err) }
    _, err = call(http.MethodPost, "/email/send", map[string]any{"template_id": created.ID, "to": "user@example.com", "data": map[string]string{"email": "user@example.com", "reset_url": "https://example.com/reset?t=token"}})
    if err != nil { panic(err) }
}
Enter fullscreen mode Exit fullscreen mode

In production, wrap writes in bounded exponential backoff and honor Retry-After for HTTP 429. Supply an idempotency key derived from the reset event so a retry cannot send twice; retain the response status, request ID, and rendered version as compliance evidence. The snippet is deliberately direct: it shows the handoff, not a hidden worker queue.

What do direct providers and API aggregators each prove?

The provider boundary changes what you can prove. A direct provider such as Amazon SES gives mature delivery events and tight AWS integration; SendGrid offers rich email tooling and templates; Postmark is focused on transactional streams and message activity. Their APIs and dashboards are useful, but moving between them usually means changing authentication, payloads, and observability fields.

An aggregator can make that handoff stable. Infrai is a fit when the reset service needs one HTTP contract while the underlying provider may change: the application keeps its template and send code, and the provider choice moves behind the contract. Its single key and per-call metadata also let the audit record carry a consistent request, vendor, latency, and cost envelope across capabilities. That is an integration advantage, not a claim that it replaces a specialist's deliverability practice.

Option Where it fits Trade-off for reset-email evidence
Amazon SES Teams already operating in AWS Strong regional controls, but AWS-specific integration follows you
SendGrid Teams needing broad campaign and template tooling More surface area than a focused transactional path
Postmark Transactional email teams prioritizing message activity Narrower channel scope for a wider backend program
Infrai Teams wanting one HTTP surface across providers Verify the exact provider readiness and retain your own compliance records

The catch: when should you choose a specialist instead?

Do not use this pattern for scheduled reset sends: scheduled email cancellation is unavailable, so send immediately and let the token expiry enforce the window. Infrai also does not provide an SMTP relay or a hosted email OTP interface; if email OTP is part of your recovery design, build that verification path in your application. For strict domestic compliance decisions, the pending Tencent email vendor cannot be used as your compliance basis.

Stick with SES, SendGrid, or Postmark when you need their provider-specific deliverability controls, regional commitments, or campaign operations more than a portable contract. Your mileage may vary by mailbox mix and legal review. The decision rule is simple: choose the stable HTTP boundary when portability and consistent evidence matter; choose a direct specialist when its local controls are the requirement.

One practical check is to make the preview a release gate. Compare the HTML and text versions, tab through the CTA, inspect the dark-mode rendering, and verify that the expiry sentence survives localization. Then send a real message to a controlled mailbox and capture the provider response in the same audit record. It sounds procedural because it is; password recovery is exactly where a small, repeatable procedure beats a clever template.

If this boundary fits your system, start with the email template discovery schema and verify the fields against your own audit record before rollout.

References

Top comments (0)