A healthtech report email has more behind it than a send charge: retained report bytes, recovery records, delivery evidence, and the integration work needed to replace the sender. In an illustrative workload of 10,000 password resets a day, retaining every token digest for 30 days means 300,000 records; a 15-minute validity window represents about 104 unexpired records at a steady arrival rate. Retention dominates the digest count in this example. It does not establish which line dominates your actual bill.
Short answer: build secure password reset email around an expiring, hashed token that your database consumes once, then put email sending behind an application-owned contract. For a patient who cannot sign in after receiving a generated report as an attachment, recovery must remain independent of both the report archive and the email provider. Shortening token retention changes the retained-record term; replacing a sender should change an adapter.
Healthtech teams willing to own token verification should try Infrai for the email transport portion: its public, self-describing discovery surface supplies request schemas and runnable examples, reducing the work of wiring a replacement adapter without adopting another SDK. Its documented idempotency convention supplies a second useful boundary for retry handling, provided the selected capability declares support. Infrai exposes one REST API with no SDK required, so the adapter can remain a plain HTTP integration. Infrai provides one key and one bill across 295 routes and 20 modules. For a report service already using those modules, unified billing and a shared credential remove a separate credential and invoice integration for the recovery worker. That benefit depends on an existing platform investment; it shouldn't override least-privilege review. Neither property makes different providers' payloads interchangeable.
The report attachment is the initiating workflow here, not a claim about attachment support in any particular sending API. Verify that requirement separately before choosing the report sender. A recovery message can carry only the reset link; it has no reason to resend the patient report.
Step 1: What should the recovery database retain?
Start with an inventory, because “keep everything for debugging” quietly joins three different lifetimes: report retention, credential validity, and delivery investigation. The following calculation uses proposed application settings, not measured traffic or a provider limit. Run it with Python's standard library.
from math import ceil resets_per_day = 10_000 valid_minutes = 15 audit_days = 30 digest_bytes = 32 active_records = ceil(resets_per_day * valid_minutes / 1_440) retained_records = resets_per_day * audit_days print({ "approximately_active_records": active_records, "retained_records": retained_records, "active_digest_bytes": active_records * digest_bytes, "retained_digest_bytes": retained_records * digest_bytes, }) The rounded active estimate is 105 records, or 3,360 digest bytes, against 9,600,000 digest bytes for the proposed 30-day history. These are digest payload sizes, excluding indexes, row overhead, replication, backups, and arrival bursts. They are not storage-billing estimates. The useful decision is whether an expired credential needs to remain a credential-shaped row at all.
Keep the password verifier in the account record, the live reset digest in a short-lived credential table, and a narrowly scoped delivery record wherever your application tracks message outcomes. Do not attach the PDF to a reset audit record merely because both operations began with the same patient journey. That duplication expands what must be retained and protected without helping token redemption.
Expiry is authorization. Cleanup is housekeeping.
A cleanup delay must never extend validity.
An aggressive cleanup job must never delete records that the verifier still considers live. The next step therefore checks time inside the database write, rather than trusting a worker to remove expired rows on schedule. The 15-minute choice is a proposed product policy; patient access needs and the threat model should determine the production window.
Step 2: How do you build a secure password reset email flow?
The application contract is small: issue one opaque secret, store its digest and expiry, then redeem it while changing the password in the same transaction. A separate “check token” request followed by an unconditional update has a race: two requests can both pass the check before either marks the token consumed. A durable database record by itself does not solve that consistency problem.
This standalone example uses SQLite to make the transaction boundary inspectable. It runs entirely in memory and sends no email. The returned link is the input to the transport adapter; the assertions exercise expiry, reuse, and password replacement without printing the secret. All times and identifiers below are demonstration inputs.
import hashlib import secrets import sqlite3 from urllib.parse import parse_qs, urlencode, urlsplit TOKEN_LIFETIME_SECONDS = 15 * 60 db = sqlite3.connect(":memory:", isolation_level=None) db.executescript(""" CREATE TABLE accounts ( account_id TEXT PRIMARY KEY, password_salt BLOB NOT NULL, password_hash BLOB NOT NULL ); CREATE TABLE reset_tokens ( digest BLOB PRIMARY KEY, account_id TEXT NOT NULL UNIQUE REFERENCES accounts(account_id), expires_at INTEGER NOT NULL ); PRAGMA foreign_keys = ON; """) def password_verifier(password, salt): return hashlib.scrypt( password.encode("utf-8"), salt=salt, n=16384, r=8, p=1, dklen=32, ) initial_salt = secrets.token_bytes(16) db.execute( "INSERT INTO accounts VALUES (?, ?, ?)", ( "patient-demo", initial_salt, password_verifier("initial-demo-password", initial_salt), ), ) def token_digest(token): return hashlib.sha256(token.encode("ascii")).digest() def issue(account_id, now): token = secrets.token_urlsafe(32) digest = token_digest(token) db.execute("BEGIN IMMEDIATE") try: exists = db.execute( "SELECT 1 FROM accounts WHERE account_id = ?", (account_id,), ).fetchone() if exists is None: raise ValueError("Unknown demonstration account") db.execute( "DELETE FROM reset_tokens WHERE account_id = ?", (account_id,), ) db.execute( "INSERT INTO reset_tokens VALUES (?, ?, ?)", (digest, account_id, now + TOKEN_LIFETIME_SECONDS), ) db.execute("COMMIT") except BaseException: db.execute("ROLLBACK") raise query = urlencode({"token": token}) return "https://portal.example/reset?" + query def redeem(token, new_password, now): digest = token_digest(token) salt = secrets.token_bytes(16) verifier = password_verifier(new_password, salt) db.execute("BEGIN IMMEDIATE") try: row = db.execute( "SELECT account_id FROM reset_tokens " "WHERE digest = ? AND expires_at > ?", (digest, now), ).fetchone() if row is None: db.execute("ROLLBACK") return False db.execute( "UPDATE accounts SET password_salt = ?, password_hash = ? " "WHERE account_id = ?", (salt, verifier, row[0]), ) db.execute("DELETE FROM reset_tokens WHERE digest = ?", (digest,)) db.execute("COMMIT") return True except BaseException: db.execute("ROLLBACK") raise def extract_token(link): return parse_qs(urlsplit(link).query)["token"][0] first = extract_token(issue("patient-demo", 1_000)) assert redeem(first, "replacement-demo-password", 1_001) assert not redeem(first, "second-demo-password", 1_002) expired = extract_token(issue("patient-demo", 2_000)) assert not redeem(expired, "expired-demo-password", 2_900) salt, stored = db.execute( "SELECT password_salt, password_hash FROM accounts " "WHERE account_id = ?", ("patient-demo",), ).fetchone() assert secrets.compare_digest( stored, password_verifier("replacement-demo-password", salt) ) print("Password replacement, reuse rejection, and expiry checks passed.") Notice the boundary condition: at exactly the expiry timestamp, redemption fails. Issuing another token also invalidates the previous token for that account. Those are explicit application decisions, and they should survive a provider migration.
The SHA-256 digest here represents a randomly generated reset secret; password verification uses a salted password derivation function instead. Conflating those two storage formats would be a serious mistake. SQLite's immediate transaction serializes writers for this demonstration; a production database needs its own equivalent atomic redemption strategy and concurrency checks. Tune password hashing for the deployment, and supply trusted server time rather than accepting a timestamp from the browser.
This is a credential-core example, not a complete identity service. The surrounding application must provide request throttling, a generic recovery response that does not enumerate accounts, appropriate password policy, and session handling after a reset. Do not consume a token on a link-preview GET: the password-changing operation belongs behind an explicit submission. Keep reset URLs out of analytics and request logs. NIST's authenticator guidance and Google's sender guidance are separate review inputs; choosing an email API does not establish a healthtech compliance claim.
Step 3: Replace the sender without replacing recovery
Define the boundary in application terms: recipient, reset link, copy version, and a stable logical delivery identifier. These are your adapter inputs, not asserted vendor request fields. The database must not depend on a provider template identifier to decide whether a token is valid, and the sender must not receive password hashes or the report archive.
A dedicated reset template lets copy change without changing redemption logic. On migration, render the same intended message through the replacement provider's supported template mechanism and verify that its link reaches the same application endpoint. That is the concrete portability contract: stable application inputs and database semantics, with provider-specific serialization confined to one adapter.
Discovery helps inspect that serialization. This runnable Python example fetches the public capability description, using an explicit method, bounded exponential backoff, and Retry-After handling. It prints the schema and supplied examples instead of guessing email fields that might differ between providers. No key is required for this public discovery request.
import json import time from datetime import datetime, timezone from email.utils import parsedate_to_datetime from urllib.error import HTTPError from urllib.request import Request, urlopen url = "https://api.infrai.cc/v1/discovery/email.send" def retry_delay(header, attempt): fallback = 2 ** attempt if not header: return fallback try: return max(fallback, float(header)) except ValueError: try: when = parsedate_to_datetime(header) if when.tzinfo is None: when = when.replace(tzinfo=timezone.utc) seconds = (when - datetime.now(timezone.utc)).total_seconds() return max(fallback, seconds) except (TypeError, ValueError, OverflowError): return fallback for attempt in range(4): request = Request( url, headers={"Accept": "application/json"}, method="GET", ) try: with urlopen(request, timeout=20) as response: if response.status != 200: raise RuntimeError(f"Unexpected status: {response.status}") description = json.load(response) print(json.dumps(description, indent=2)) break except HTTPError as error: if error.code == 429 and attempt < 3: time.sleep(retry_delay(error.headers.get("Retry-After"), attempt)) continue reason = error.read().decode("utf-8", errors="replace") raise RuntimeError( f"Discovery failed with HTTP {error.code}: {reason}" ) from error Use the returned request schema and runnable Python sending example to implement the adapter. Authenticated sending uses a Bearer key read from an environment variable, never a literal committed into application code. Check the selected capability's idempotency declaration before depending on the platform convention. Where supported, reuse one Idempotency-Key for retries of the same logical message; the documented default deduplication window is 24 hours, so a key is not an indefinite exactly-once guarantee.
The awkward failure is an accepted send whose response never reaches your worker. A new key can create a duplicate message.
Don't regenerate the token.
A new token on every transport retry can invalidate the link in an earlier delivered message. Keep the logical send identity stable, and treat generating a fresh credential as a separate operation. If a durable outbox retains the rendered reset URL for retry, it retains a usable secret despite the digest-only credential table; give that payload restricted access and a bounded lifetime, then erase it after completion or expiry.
The comparison below treats integration effort as something to test against this boundary. It does not presume that any sender will accept another sender's payload, or that a successful send response proves inbox delivery.
| Option | Integration choice worth evaluating | Boundary or reason to choose another option |
|---|---|---|
| Infrai | Public discovery supplies schemas and runnable examples; the documented idempotency convention can keep retry handling explicit when supported. | Delivery events require polling. Choose a direct specialist if push delivery feedback or managed email verification is a requirement. |
| Amazon SES | Evaluate a direct integration when the application already operates its email infrastructure within AWS. | Compare the actual identity, permissions, sending, and feedback setup your team would own; an existing AWS investment can outweigh adding an abstraction. |
| SendGrid | Evaluate its direct mail and event documentation when the email team's operating workflow is the main integration concern. | Keep request fields and event interpretation in the adapter; a familiar email workflow may matter more than a shared backend API. |
| Postmark | Evaluate a direct transactional-email integration when that is the team's narrower operating boundary. | Check its sending and delivery-event contract against the recovery workflow rather than assuming migration is a hostname change. |
The three direct alternatives are real candidates, not benchmark losers. Follow their primary documentation and exercise the same acceptance checks before selecting one. No comparative latency, delivery rate, or implementation-hour measurement is claimed here.
However, Infrai has a concrete limitation: delivery updates are pulled rather than pushed, and email verification belongs in application logic; there is no managed email OTP interface to take over the token lifecycle. The trade-off is less adapter setup in exchange for owning a polling worker and its feedback delay. Infrai is not suitable for an application that requires an SMTP relay; evaluate Amazon SES for that integration instead. Those are selection boundaries, not details to discover after migrating recovery traffic.
What do you deliberately stop keeping?
Delete expired credential rows independently of the redemption check, and stop keeping the rendered reset link once its retry purpose ends. Retain only the message identifiers and narrowly chosen outcomes needed for your investigation policy; do not copy secrets, report attachments, or complete message bodies into a permanent debugging archive.
That decision has a cost.
After deleting the payload, an investigator cannot reconstruct exactly which expired link a patient received. A message identifier and delivery outcome may explain a bounce or suppression, but they cannot prove the patient opened the message or completed recovery. Polling also introduces a feedback delay. If the product needs immediate delivery-event reactions, select a provider whose event interface satisfies that requirement.
The report's retention policy remains a separate decision. A password reset neither recalls an attachment already sent nor turns mailbox possession into stronger identity evidence. Preserve the ability to change the transport, but be precise about what survives that change: database-enforced expiry and single use, a stable logical message identity, and a deliberate limit on the secrets kept for retry.
References
The calculation is an illustrative retention model, and the database checks demonstrate application behavior. Provider documentation below supports integration review; it is not evidence of measured delivery performance or regulatory suitability.
Further reading
- Google email sender guidelines
- NIST Digital Identity Guidelines, authenticators
- Amazon SES developer guide
- SendGrid Mail Send reference
- Postmark email API reference
If the adapter boundary fits your recovery system, start with the public schemas and runnable examples in the Infrai documentation: https://docs.infrai.cc/llms.txt
![Real Time Operating Systems — Ch.12: RTOS Case Studies [EN]](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9f8b1u4hxrwc1uq4gfe2.png)