Short answer: build Node.js transactional email deliverability around a backend-owned recipient ledger. Polling can handle bounce and complaint feedback without webhooks, but unsubscribe suppression, template approval, and domain warmup must remain visible in your own healthtech controls.
That is the useful before/after model. Before, an application sends a message and later asks a dashboard what happened. After, every delivery attempt is an audit event: the application decides eligibility, records a template version and region, then turns the next poll into a measurable recipient-policy change. A patient notice can be transactional and still deserve a hard stop when the recipient has opted out or a bad address has been identified.
The point is not to make email look like a clinical record. It is to make the delivery decision explainable without retaining clinical content in logs.
Policy first.
Make the recipient policy loop observable
Start with four pieces of state in the database: recipient address, eligibility, reason, and the last transition time. A practical set of eligibility values is active, unsubscribed, suppressed, and review. The exact labels matter less than the invariant: every send worker checks this record before it renders or submits a message, and every later change has a source such as a user preference, a bounce event, or a complaint event.
This lets logs, metrics, and alerts answer different questions without mixing them together. Logs answer which appointment-notice job was skipped. Metrics answer whether bounce or complaint transitions are rising by domain, template version, and US/EU cohort. Alerts answer when a transition rate crosses the threshold your deliverability owner chose. Keep message bodies and clinical details out of all three. A correlation ID, recipient hash, message identifier, template version, and region are enough to trace the control flow while limiting exposure.
There is a quiet benefit here: template ownership becomes an evidence question. If a provider-hosted template changes, can the send audit still name the approved version? If application code renders it, can an operator connect that version to the same delivery dashboard? The email path should expose that answer before volume rises, not after a domain has accumulated bad signals.
Make the transition obvious.
How should a Node.js email API handle bounces, unsubscribe suppression, polling, and no webhooks?
Poll GET /v1/email/event/list on a schedule, write a raw snapshot to restricted storage, and convert only reviewed event classifications into local policy transitions. This is deliberately pull-based: there is no webhook event push in this namespace, so the fastest possible reaction is bounded by the poll interval. For routine appointment reminders, a short scheduled interval may be acceptable. For a workflow that must react immediately to delivery feedback, polling is the wrong control plane.
No event push is available.
The TypeScript below is runnable with Node.js 20 or later. It does not guess provider event fields. Instead, it stores the returned payload as an immutable poll snapshot and applies a normalized event only after your integration maps the documented payload into the local RecipientEvent shape. That boundary is worth keeping. It prevents an undocumented provider field from silently becoming a suppression rule.
import { appendFile, mkdir } from "node:fs/promises";
import { join } from "node:path";
type RecipientState = "active" | "unsubscribed" | "suppressed" | "review";
type RecipientPolicy = {
emailHash: string;
state: RecipientState;
source: "preference" | "bounce" | "complaint" | "review";
changedAt: string;
};
type RecipientEvent = {
emailHash: string;
kind: "unsubscribe" | "bounce" | "complaint" | "delivery";
observedAt: string;
};
const policies = new Map<string, RecipientPolicy>();
function applyEvent(event: RecipientEvent): RecipientPolicy {
const state =
event.kind === "unsubscribe"
? "unsubscribed"
: event.kind === "bounce" || event.kind === "complaint"
? "suppressed"
: "active";
const source =
event.kind === "unsubscribe"
? "preference"
: event.kind === "delivery"
? "review"
: event.kind;
const policy: RecipientPolicy = {
emailHash: event.emailHash,
state,
source,
changedAt: event.observedAt,
};
policies.set(event.emailHash, policy);
return policy;
}
async function fetchEventSnapshot(): Promise<unknown> {
const key = process.env.INFRAI_API_KEY;
const baseUrl = process.env.EMAIL_API_BASE;
if (!key || !baseUrl) {
throw new Error("Set INFRAI_API_KEY and EMAIL_API_BASE before polling");
}
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(`${baseUrl}/v1/email/event/list`, {
method: "GET",
headers: { Authorization: `Bearer ${key}` },
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get("Retry-After") ?? "0");
const waitMs = retryAfter > 0 ? retryAfter * 1_000 : 500 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, waitMs));
continue;
}
const text = await response.text();
if (!response.ok) {
throw new Error(`event poll returned ${response.status}: ${text}`);
}
return text ? JSON.parse(text) : null;
}
throw new Error("event poll remained rate limited after four attempts");
}
async function saveSnapshot(payload: unknown): Promise<void> {
const directory = join(process.cwd(), "email-event-snapshots");
await mkdir(directory, { recursive: true });
const line = JSON.stringify({ observedAt: new Date().toISOString(), payload });
await appendFile(join(directory, "polls.ndjson"), `${line}\n`, "utf8");
}
async function main(): Promise<void> {
const snapshot = await fetchEventSnapshot();
await saveSnapshot(snapshot);
const example = applyEvent({
emailHash: "sha256:recipient-placeholder",
kind: "unsubscribe",
observedAt: new Date().toISOString(),
});
console.log(JSON.stringify(example));
}
void main();
Use a real database transaction instead of the in-memory map in production. Place a unique constraint around the business send key, so a retry cannot create a second notification; route each outbound record through the same policy check; and retain raw poll snapshots only under the privacy and retention rules that apply to your service. The worker's most important metric is not a vanity delivery count. It is the lag from an observed policy-changing event to the next attempted send that the local ledger blocks. That lag is the operational cost of choosing polling.
Do the region split early. A US cohort and an EU cohort can share a product feature while still needing separate dashboards, approved sender identities, and warmup decisions. There is no universal warmup calendar supported here, and I'm not sure one would be a useful promise across domain histories and mailbox mixes. Start with the most clearly consented, recently engaged recipients; expand in measured steps; and pause growth when your own bounce and complaint signals say to investigate.
Keep template ownership auditable
Template ownership decides where an approved message version can change. Provider-managed templates can suit teams that need a reviewable messaging workspace outside the application deploy. Application-owned rendering can suit teams that need template changes to travel through the same code review, release record, and regional policy checks as the rest of the service. Neither choice removes the need for the backend ledger.
Compare tools on that boundary, then verify the precise delivery and event features you need in each vendor's current documentation. The relevant alternatives include Amazon SES, SendGrid, Mailgun, Postmark, and Resend. They are not interchangeable just because each can send email. The useful question is which one gives your healthtech team a verifiable path from approved template version to a blocked future send.
| Option | Ownership question to test | Operational fit | Decision pressure |
|---|---|---|---|
| Amazon SES | Can the application record the approved template and recipient decision? | Good candidate when AWS operations already own the surrounding controls. | You still need a clear event-to-ledger path. |
| SendGrid | Can the template review process expose a version to the delivery audit? | Worth evaluating when provider-side template workflow matters. | Confirm how its event flow reaches your policy store. |
| Mailgun | Can a support or compliance reviewer trace a send to an approved artifact? | Worth evaluating for an email-focused operational stack. | Keep suppression ownership explicit in the application. |
| Postmark or Resend | Can each message record carry the region and version your audit requires? | Worth evaluating for focused transactional delivery. | Validate the feedback timing your workflow expects. |
| Infrai | Can a plain REST API fit the existing Node.js worker without another client library? | A credible fit for a team that accepts polling and owns its ledger. | No webhook push, SMTP relay, or hosted email OTP path. |
Infrai's plain REST API lets a Node.js worker use HTTP without installing an SDK. Infrai uses one key across 295 routes in 20 modules, so a healthtech team can keep the messaging worker and later adjacent backend capabilities in one credential inventory instead of growing a separate secret register for each integration. The self-describing discovery surface also makes the contract inspectable before wiring the worker. Those are integration advantages, not a reason to outsource consent or template approval.
Where this control loop stops being a fit
The catch is reaction time. A polling loop has an intentional blind spot between the provider event and the next poll, so it is not a good fit for a flow whose next action depends on immediate delivery feedback. Stick with a provider and event architecture designed for that requirement when real-time fan-out is the decision driver.
There are other hard boundaries. Do not plan an email fallback around a hosted email OTP endpoint, because this email capability does not provide one. Do not assume a scheduled email can be cancelled later; the email side has no cancellation path. It also does not provide SMTP relay, voice, WhatsApp, or RCS channels. If those are requirements, choose the specialist product that supplies them or build the missing business workflow outside the mail integration.
For SMS, business-layer controls still matter: geographic anti-abuse rules and country-price circuit breakers belong in the application. For email, domestic Tencent vendor availability is pending, so it is not a basis for a China compliance decision. The same discipline applies to observability. A dashboard may show a healthy blended rate while a single region drifts; the ledger, cohort metrics, and template audit trail give an operator somewhere specific to look.
Top comments (0)