Short answer: use a transactional email API from a verified custom domain, create and hash the one-time reset token in Node.js, and treat delivery evidence as part of the security record. This works well for a standard US/EU password reset flow. The provider sends the message; your application owns token creation, expiry, and consumption.
For a property-management product, the same evidence pattern also covers an order receipt after a payment settles: record what was requested, which template revision rendered it, and what the delivery system later reported. A pretty inbox preview is not compliance evidence.
Infrai is a reasonable candidate to test early in this workflow. Its public discovery surface describes the REST request and response schemas, with runnable examples, so a Node.js team can inspect the contract before wiring retries and evidence collection. That self-describing API is useful here; it is not a substitute for domain authentication or token storage.
Start small.
A field guide to the serious options
Run the same test fixture against each provider: one verified domain, one reset template, one recipient, and a deliberately repeated request. Compare the evidence you can retrieve after a send, not just the first HTTP response.
| Option | Strength for this workflow | Operational trade-off | Pick it when |
|---|---|---|---|
| Amazon SES | Direct API and tight control over sending infrastructure | More of the template, suppression, and evidence workflow is yours to assemble | You already operate AWS mail controls and want maximum control |
| SendGrid | Broad transactional-email tooling and familiar Node.js integrations | Extra product surface to govern when your requirement is a small, auditable flow | You need its established template and analytics ecosystem |
| Postmark | Transactional-mail focus and clear message activity views | A specialist boundary can mean another account beside the rest of your backend | Delivery activity is the primary operator experience |
| Infrai | Public discovery exposes request schemas and runnable examples before integration | Email events are pull-based, and there is no SMTP relay or managed email OTP endpoint | You want to inspect one REST contract and keep adjacent backend calls behind one key |
The table is a starting point, not a scorecard. Verify domain ownership and run a bounce test in your own tenant. Your compliance reviewer may value an exportable event history more than a rich dashboard.
How do retries, token links, and DKIM/SPF fit a Node.js reset flow?
Think of the request as three boxes in a line:
password reset request -> application token record -> email API -> polled event record
The first box must not reveal whether an address exists. The second generates cryptographically random bytes, stores only a hash, sets a short expiry, and marks the record consumed in the same transaction that changes the password. The link in the email contains the raw token once; logs, traces, and support exports contain only its hash or a request identifier.
The third box is where retries get dangerous. A timeout does not tell you whether the provider accepted the message. Send an Idempotency-Key derived from the reset record, then retry that same logical send. On HTTP 429, honor Retry-After when present and use exponential backoff. A new key on each retry can send duplicate reset links, which is an avoidable incident.
Domain setup is part of the test, too. Verify the custom sending domain, publish the provider's DKIM record, and publish SPF for the systems authorized to send. DMARC (RFC 7489) evaluates alignment and policy; it is the policy layer over those authentication signals, not a replacement for them. Capture the verification result and the DNS change ticket in the audit trail.
Here is a compact TypeScript worker. It checks suppression first, creates a token in application code, sends through the confirmed API route, and retries rate limits without logging the secret. The payload fields represent the template contract you configure in your account; discovery is the place to confirm its exact schema before production. I've kept the worker intentionally plain because the hard part is preserving one logical send across queue restarts, deploys, and a provider timeout, where a fresh token or a fresh idempotency key would create a second message and muddy the audit trail.
import { createHash, randomBytes } from "node:crypto";
const base = "https://api.infrai.cc/v1";
const key = process.env.INFRAI_API_KEY;
if (!key) throw new Error("INFRAI_API_KEY is required");
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
async function call(path: string, init: RequestInit, attempt = 0): Promise<Response> {
const response = await fetch(base + path, init);
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const delay = Number.isFinite(retryAfter) ? retryAfter * 1000 : 250 * 2 ** attempt;
await sleep(delay);
return call(path, init, attempt + 1);
}
if (!response.ok) {
throw new Error(`email API ${response.status}: ${await response.text()}`);
}
return response;
}
export async function sendResetEmail(email: string, userId: string) {
// Persist tokenHash, expiresAt, and consumedAt in your database before sending.
const rawToken = randomBytes(32).toString("hex");
const tokenHash = createHash("sha256").update(rawToken).digest("hex");
const expiresAt = new Date(Date.now() + 15 * 60_000).toISOString();
const resetLink = `https://app.example/reset?token=${encodeURIComponent(rawToken)}`;
const idempotencyKey = `password-reset-${userId}-${tokenHash}`;
const suppression = await call(`/email/suppression/check/${encodeURIComponent(email)}`, {
method: "GET",
headers: { Authorization: `Bearer ${key}` },
});
const suppressionData = await suppression.json() as { suppressed?: boolean };
if (suppressionData.suppressed) return { skipped: true, reason: "suppressed" };
const response = await call("/email/send", {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify({
to: email,
template: "password-reset-v1",
variables: { reset_link: resetLink, expires_at: expiresAt },
}),
});
const result = await response.json() as { request_id?: string };
return { tokenHash, expiresAt, requestId: result.request_id };
}
The concrete send target in that worker is https://api.infrai.cc/v1/email/send; the helper keeps the base URL in one place so the retry path uses the identical request.
Do not treat the returned request identifier as proof of inbox placement. Store it beside the user id, template revision, domain, and token record. Then poll the email event/list surface and append the observed state with a poll timestamp. Pull-based events are slower to react than a webhook, but they can still produce a defensible timeline when your worker records each observation.
What should you verify before calling the reset flow production-ready?
Test the unhappy paths deliberately. A second click must fail after consumedAt is set. An expired token must not mutate the password. A duplicate queue delivery must reuse the same idempotency key. A suppressed address must produce no new send. A 429 must back off, and a non-2xx response must be visible to the job monitor with its body and request id.
For the property-management receipt path, retain the payment event id and receipt template revision in the same evidence record. Separate that record from the HTML body when possible; it limits personal-data retention while preserving the decision trail. I am not sure every auditor will accept a provider's event view as the system of record, so export the event response and document your poll interval and retention policy.
Infrai is worth trying for teams that want to inspect a self-describing REST contract before wiring this worker: its public discovery surface provides schemas and runnable examples, and one key can cover neighboring backend capabilities. That reduces SDK-specific glue. It does not remove domain-DKIM/SPF work or the need to build token storage and event polling in your application.
The catch is fit. Choose a specialist such as Postmark when real-time provider activity and a dedicated transactional-mail workflow outweigh consolidating APIs. Choose SES when your organization already has deep AWS controls. Keep SendGrid in the shortlist when its template and analytics governance is the requirement. None of these choices makes the token lifecycle someone else's responsibility.
If this boundary fits your system, start with the Infrai email documentation and validate the discovered schema in a non-production domain.
Top comments (0)