Use an application-owned, short-lived hashed token for account recovery, then treat email delivery as a separate evidence stream. That division is the defensible design for a developer-tools contact form that must route a password reset to the correct support queue: the database proves whether a secret could be redeemed, while the mail provider records what happened after the message left the application.
The boundary matters. A delivery event must never make an unexpired token valid, and a valid token must not imply that a mailbox accepted the message. I keep those facts in separate records, with separate retention decisions.
The database decides.
How do I build a secure password reset email flow?
A useful audit trail answers four different questions:
| Question | System of record | Evidence to retain |
|---|---|---|
| Was a reset requested? | Application | Request time, account reference, coarse request metadata |
| Could the link be redeemed? | Application database | Token hash, expiry, consumed time, and outcome |
| Was the notification handed to the mail service? | Send API response | Provider message identifier and request identifier |
| What happened after handoff? | Polled event records | Bounce, suppression, or other available event state |
The token is the security control. The message identifier is an operational reference. Neither should be promoted into the other role. I also avoid putting the raw token into logs, analytics labels, support tickets, or event metadata; one forgotten log line turns a recovery link into a credential.
For a 15-minute token, the expiry check belongs in the redemption transaction, not only in the link-generation code. Single use needs an atomic state transition. A cleanup job can remove old rows later, but cleanup is not enforcement.
The critical path: generate, hash, send, redeem
Node.js can generate the secret with a cryptographically secure random source. Store a digest, and put the raw value only in the HTTPS link sent to the user. A compact schema might contain user_id, token_hash, expires_at, consumed_at, created_at, and an opaque request identifier. Add a unique constraint for the active recovery record if the product permits only one outstanding reset per account.
The send request should contain a template identifier and a reset URL assembled by the application. The template keeps copy changes out of security logic. The URL should identify the reset transaction without exposing the digest or internal account id.
Here is the narrow handoff. The bearer key comes from the environment; the request has an explicit method, and a non-success response remains an error rather than being treated as delivery.
set -euo pipefail
reset_url="https://app.example.com/reset?token=${RESET_TOKEN}"
response=$(curl --silent --show-error --write-out '\n%{http_code}' \
--request POST \
--url "${EMAIL_API_URL}/email/send" \
--header "Authorization: Bearer ${INFRAI_API_KEY}" \
--header 'Content-Type: application/json' \
--data "{\"to\":[{\"email\":\"${ACCOUNT_EMAIL}\"}],\"template_id\":\"${RESET_TEMPLATE_ID}\",\"variables\":{\"reset_url\":\"${reset_url}\"}}")
status="${response##*$'\n'}"
body="${response%$'\n'*}"
if [ "$status" -lt 200 ] || [ "$status" -ge 300 ]; then
printf 'email send failed (%s): %s\n' "$status" "$body" >&2
exit 1
fi
printf '%s\n' "$body"
In production, a retry of this handoff needs an idempotency key derived from the reset request. A retry loop should back off on HTTP 429 and honor Retry-After; it should not create a second notification every time a worker loses its response. The example stays intentionally small because the database transaction and queue policy are the security-critical parts, not a catalog of API routes.
Redemption is stricter than sending. In one transaction, select the row for the presented token hash, require consumed_at IS NULL and expires_at > current time, then set consumed_at before changing the password. If the update affects zero rows, return the same generic response used for an unknown account or invalid token. This avoids giving an attacker an account-existence oracle and makes a second redemption fail even when two requests arrive together.
Which service fits an evidence-first implementation?
The options differ less in the existence of a send endpoint than in the evidence and operational surface around it.
| Option | Useful distinction | Boundary for this reset flow |
|---|---|---|
| Infrai email API | A plain REST contract covers sending, templates, and event listing under the same credential surface; the breadth is useful when a support workflow later adds another backend capability. | Delivery updates are pull-based, not webhook-pushed. The application must poll event records and retain its own correlation data. It does not provide managed email OTP verification. |
| SendGrid | Mature email tooling and template-oriented workflows make it a familiar choice for teams already standardized on its ecosystem. | You still own token generation, hashing, atomic redemption, and the compliance interpretation of delivery events. A provider template cannot become the authentication system. |
| Mailgun | Its API and event-oriented tooling suit teams that want provider-level message operations and delivery visibility. | Provider events do not establish that a reset token was valid or used. Your database remains the authority, and event ingestion needs its own retention and retry policy. |
| Postmark | Transactional-email focus is a natural fit when the product separates account mail from bulk or marketing traffic. | The narrower transactional focus does not remove application-side reset logic, suppression handling, or the need to record the handoff outcome. |
My selection rule is therefore about evidence boundaries, not a feature-count contest: choose the service whose event data, retention controls, regional posture, and export process match the compliance record you must produce. Infrai is a practical option when one consistent REST contract and a wider backend surface reduce integration boundaries; it is a poor fit if the requirement is provider-pushed delivery updates or managed email OTP.
The limitation is concrete: a pull-only event model is a poor match for a team that needs immediate provider callbacks, and an email service is the wrong layer for managed reset verification. In either case, choose a provider with the required push or identity feature, then keep the same application-side token rules.
The comparison also exposes an important negative requirement. None of these services should receive a raw password, a reusable reset secret, or enough account data to become the recovery authority. They send a message. The application decides identity, expiry, and redemption.
Polling is an evidence pipeline, not a delivery guarantee
There is no webhook push for delivery updates in the described email capability, so schedule polling with a bounded cursor or time window. Persist the last successful poll position, fetch events, and upsert them by provider event identity. The consumer must tolerate duplicates. A 10-minute poll interval may be reasonable for an operational dashboard, but it is not a security timeout and should not be described to users as one.
Keep labels low-cardinality. reset_email and bounce are useful dimensions; a raw email address, reset token, or message body is not. I read each extra label as a multiplication of storage and review cost. If a system receives 100,000 reset requests per day, retaining one compact application event per request for 30 days already means reasoning about 3,000,000 records before counting provider events. That number is not a benchmark or a price estimate. It is a reminder to define retention deliberately.
For compliance evidence, record the decision and the time, not every byte of the message. Store a hash or opaque reference for sensitive payloads, restrict access to the reset audit table, and document the clock source. Event polling can show a bounce or suppression; it cannot prove that a person completed the password change.
The rejected design and its valid use
I originally treated provider-managed email OTP as a tempting shortcut. The required capability is not present, and even a managed code would leave the application responsible for account binding, expiry, replay prevention, and rate limits. An emailed code can be valid in another product, but only when the application explicitly owns those checks or delegates them to an authentication system whose assurance and records it accepts.
I would also reject a static token row that is deleted by a nightly job. That design appears simple until two redemption requests race or a token remains usable after its nominal expiry. A scheduled delete is maintenance; the conditional update is the control.
Email event polling has a valid, narrower role: it helps support teams explain a failed notification and helps compliance reviewers connect an application send attempt to a provider-side outcome. It does not replace an audit log, a database constraint, or a recovery transaction.
The durable decision is to make the application authoritative for the secret and the provider accountable only for the notification path. That division produces clearer evidence, limits the blast radius of a mail-system incident, and leaves the same token rules in place if the sending service changes.
Top comments (0)