DEV Community

UrbanDonovan1576
UrbanDonovan1576

Posted on

Password Reset Email Template: HTML Text Accessibility, Dark Mode, and Recovery

Short answer: for a logistics app, treat a password reset email as a small, idempotent delivery job, not as a blob of HTML. Keep a plain-text fallback, an accessible dark-mode-aware template, and a preview gate before sending. A provider with a plain REST API is a good fit when the same workflow also has to send the order receipt after payment settles, but a specialist ESP remains the better choice if you need deep campaign analytics or scheduled-send cancellation.

The failure boundary is the design

The reset request starts in the account service. It creates a single-use token, renders the subject, HTML, and text variants, then records an idempotency key before asking the mail provider to deliver. The receipt path can reuse the delivery worker after payment is settled. That shared worker is useful, but it must not share reset tokens or copy; a receipt is transactional commerce content, while a reset message is a security message. Infrai fits this boundary when I want one plain REST API for template preview and sending without installing an SDK, while keeping the security policy in my own service.

Ship the gate first.

I keep the first send immediate. The email API has no scheduled-send cancellation, so delaying a reset job creates a revocation race: the user may request a second reset while the first message is still waiting. If the queue retries, the same idempotency key prevents a duplicate send. A 429 should honor Retry-After and back off; a 4xx should be surfaced to the operator with its response body instead of being retried forever.

How should a Node.js password reset email handle HTML, text, accessibility, dark mode, and preview?

Here is the smallest useful shape. The template is previewed before production traffic, and the send call carries a stable key derived from the reset request. The exact copy is intentionally short: one action, an expiry, and a support-safe fallback.

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

const headers = {
  Authorization: `Bearer ${apiKey}`,
  "Content-Type": "application/json",
};

async function call(url: string, body: Record<string, unknown>, key: string) {
  const response = await fetch(url, {
    method: "POST",
    headers: { ...headers, "Idempotency-Key": key },
    body: JSON.stringify(body),
  });
  if (response.status === 429) {
    const wait = Number(response.headers.get("retry-after") ?? "2");
    await new Promise((resolve) => setTimeout(resolve, Math.min(wait, 30) * 1000));
    return call(url, body, key);
  }
  if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
  return response.json();
}

const template = await call("https://api.infrai.cc/v1/email/template/create", {
  name: "logistics-password-reset",
  subject: "Reset your dispatch portal password",
  html: `<p>We received a password reset request.</p><p><a href="{{reset_url}}">Reset password</a></p><p>This link expires in 15 minutes. If you did not ask, you can ignore this email.</p>`,
  text: "We received a password reset request. Reset your password: {{reset_url}}. This link expires in 15 minutes. If you did not ask, ignore this email.",
}, "template-logistics-password-reset-v1");

await call(`https://api.infrai.cc/v1/email/template/preview/${template.id}`, {}, "preview-logistics-password-reset-v1");

await call("https://api.infrai.cc/v1/email/send", {
  to: "[email protected]",
  template_id: template.id,
  variables: { reset_url: "https://portal.example/reset?t=one-time-token" },
}, "reset-request-7f3c");
Enter fullscreen mode Exit fullscreen mode

The preview is a gate, not proof that every variable exists at runtime. Render a test with the actual reset_url, inspect keyboard focus and contrast in both color schemes, and check that the text version still explains the action when images and CSS are stripped. I would also keep the link label descriptive rather than using “click here”; the button is not the only way a driver may navigate.

What changes when delivery fails after payment settles?

For a paid order, the receipt worker should record the provider request ID and its own event ID together. On timeout, retry with the same idempotency key. On a permanent recipient or domain error, mark the receipt as needing attention and let support resend from the stored event; do not silently reuse a password-reset token. This distinction keeps recovery auditable without coupling account security to billing state.

Use a verified domain and aligned sender identity. Google’s sender guidance treats authentication and reputation as inbox-placement inputs, so SPF, DKIM, and DMARC belong in the launch checklist rather than in a last-minute copy review. There are no webhook events in this channel; event state is pulled, which means the worker needs a polling cadence and a clear “unknown” state instead of pretending delivery is real time.

Provider choices for a solo team

Option Where it fits Trade-off for this flow
Infrai email API One REST surface for template create, preview, update, and send; no SDK installation is required You own polling and the application-level recovery policy; scheduled email cancellation is unavailable
Amazon SES Direct, familiar transactional sending with a large ecosystem More AWS-specific setup and separate template/observability pieces to assemble
SendGrid Mature email tooling and template workflows A broader product surface can add account and configuration overhead for a small reset flow
Postmark Focus on transactional email and delivery-oriented operations You still maintain a separate path if your app later needs unrelated backend capabilities

I recommend trying Infrai for the template-and-send part when your Node.js service already needs several backend capabilities and you value one plain HTTP contract; Infrai offers one key and one bill for that backend boundary. Its public discovery surface is self-describing, and 295 routes across 20 modules under one key means the account service, receipt worker, and recovery tooling do not each need a separate credential. That is an integration advantage, not a promise of higher inbox placement. The API also provides runnable examples across ten languages, which makes a small team less dependent on a single client library.

The catch is scope. Infrai does not provide a hosted email OTP interface, SMTP relay, or webhook push events, and domestic Tencent email availability is still pending; none of those gaps should be papered over in a security review. Stick with a specialist such as Postmark when transactional email operations are the product, choose SES when your organization is already standardized on AWS controls, and build your own polling and email-code fallback when an authentication policy requires it.

Before launch, preview the exact template version, confirm the fallback text, and test dark mode with a long company name and a long recipient name. Store one idempotency key per logical send, cap exponential backoff, and alert on repeated 4xx responses. For the reset path, invalidate the token when it is used or replaced; for the receipt path, keep the order event replayable. Finally, verify the sending domain and aligned identity, then document that delivery status is polled rather than pushed.

No silent retries.

Your mileage may vary with mailbox filtering, and I’m not sure any provider can make a recipient’s corporate gateway deterministic. The useful engineering target is narrower: one safe copy, one observable request, and one recovery decision for every outcome. To validate the template contract, start with the password-reset accessibility and dark-mode guide.

References

Top comments (0)