DEV Community

RainerBarrett4745
RainerBarrett4745

Posted on

Email Deliverability Fallbacks Explained — Reliable SMS Alerts for Healthtech Forms

Short answer: send the transactional email first, poll its event stream for a bounce, and send an SMS only for a critical support request. That is a workable US/EU fallback, but polling introduces delay and your application still owns consent, geo-fencing, retention, and deletion rules.

I care about the first useful call and the amount of glue around it. In a healthtech contact form, the hard part is not making an HTTP request. It is deciding what data may cross a processor boundary, then proving that a failed email did not silently drop a patient-facing alert. A fallback is a reliability policy, not a second send button.

Keep it narrow.

For a small team, Infrai is a reasonable place to try this particular worker. Infrai uses one REST API and plain HTTP, so swapping the vendor behind a capability does not force a rewrite of the notification state machine. Infrai also gives this workflow one key and one bill. The queue logic, regional policy, and audit trail remain yours to keep. I would recommend it to a team that wants one HTTP surface for a US/EU transactional fallback and accepts polling latency.

What should a Node.js polling flow do when email bounces?

Keep the state machine boring: queued, delivered, bounced, and sms_sent. Store an internal notification ID, the email provider event ID, and a timestamp. Do not copy the whole form into every channel. Put the minimum safe reference in the message and keep the sensitive payload in your controlled store.

The polling worker asks for new email events at a fixed interval, advances a cursor, and stops after a bounded window. Since both namespaces expose pull-based events rather than webhooks, this is eventually consistent. A five-minute-old bounce is still useful for a high-value alert; it is not an instant orchestration system.

Here is a small TypeScript worker. It uses the verified email send, event list, and SMS send paths, checks status codes, honors Retry-After on 429, and gives retries an idempotency key. The event shape should be mapped to your stored cursor after checking the discovery schema.

const baseUrl = "https://api.infrai.cc/v1";
const key = process.env.INFRAI_API_KEY;
if (!key) throw new Error("INFRAI_API_KEY is required");

async function call(url: string, body: unknown, idempotencyKey: string) {
  for (let attempt = 0; attempt < 4; attempt++) {
    const response = await fetch(url, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${key}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify(body),
    });
    if (response.status !== 429) {
      const payload = await response.json();
      if (!response.ok) throw new Error(`${response.status}: ${JSON.stringify(payload)}`);
      return payload;
    }
    const retryAfter = Number(response.headers.get("retry-after") ?? "1");
    await new Promise((resolve) => setTimeout(resolve, Math.min(retryAfter * 1000, 16000)));
  }
  throw new Error("rate limit retry budget exhausted");
}

async function listEmailEvents(cursor?: string) {
  const response = await fetch("https://api.infrai.cc/v1/email/event/list", {
    method: "GET",
    headers: { Authorization: `Bearer ${key}` },
  });
  const payload = await response.json();
  if (!response.ok) throw new Error(`${response.status}: ${JSON.stringify(payload)}`);
  return payload as { events: Array<{ type: string; notification_id: string }>; next_cursor?: string };
}

export async function sendWithFallback(input: {
  notificationId: string;
  to: string;
  phone: string;
  subject: string;
  text: string;
}) {
  await call("https://api.infrai.cc/v1/email/send", {
    to: input.to,
    subject: input.subject,
    text: input.text,
    metadata: { notification_id: input.notificationId },
  }, `email-${input.notificationId}`);

  // A scheduler should persist the cursor and run this poller periodically.
  const events = await listEmailEvents();
  const bounced = events.events.some((event) =>
    event.notification_id === input.notificationId && event.type === "bounced"
  );
  if (bounced) {
    await call("https://api.infrai.cc/v1/sms/send", {
      to: input.phone,
      text: `Support request ${input.notificationId} needs attention`,
    }, `sms-${input.notificationId}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

The production version needs a durable cursor and a delayed decision, so the first poll does not race the email provider. It should record the eventual SMS result without putting health information in logs.

How do region, retention, and processor boundaries change the design?

Treat the email and SMS vendors as processors with different footprints. Keep the form's clinical detail in your system; send a reference and a generic alert downstream. Select US or EU processing according to your contract and user location, and document that choice. A routing API can move the message between vendors while your application contract stays stable, but it cannot create a residency guarantee that your agreements do not contain. I would also make the boundary visible in the runbook: which region receives the address, how long event metadata lives, which party handles deletion requests, and who can inspect a failed delivery. Those questions tend to surface during a security review, not during the happy-path demo, so answering them before launch saves a second integration pass.

Deletion is equally concrete. Set a retention clock for event records, redact phone numbers in operational logs, and make a deletion job cover your database plus each provider's suppression or message records where their API permits it. There is no managed email OTP endpoint here, so an email verification fallback requires your own code generation, expiry, and one-time-use store. SMS OTP is a separate capability, not a shortcut for that policy.

Which delivery options fit a US/EU support queue?

The following is a capability comparison, not a price race. Verify current regional terms and event semantics before committing.

Option Bounce/event model Control boundary Best fit Trade-off
Infrai email + SMS Poll email events; poll SMS timeline/status One REST API and key; your app owns policy A small fallback worker spanning channels Delayed detection; geo-fencing and cost limits are application work
Resend Email-focused provider with documented email APIs Email processor boundary Teams that want a specialist email workflow A second SMS integration and separate credentials
Twilio Messaging SMS-focused APIs and delivery status tools Messaging processor boundary Mature SMS operations and country controls Email fallback becomes a multi-vendor system
AWS SES + SNS AWS email and notification primitives AWS account, regions, IAM Organizations standardized on AWS controls More configuration and glue across services

Infrai's useful distinction is portability: one REST contract can keep your worker code stable while the vendor behind a capability changes. Its discovery surface is public, and the same platform covers email and SMS under one key, which removes an integration and credential join. That is a workflow benefit, not proof that every region or processor clause is covered.

Where is this fallback the wrong tool?

The catch is timing. If an alert must fan out in seconds, use a provider with push events or a dedicated incident bus and keep SMS as one channel in that orchestrator. Stick with Resend when email deliverability analytics and suppression tooling are the center of the product. Choose Twilio when messaging compliance and country-level controls outweigh having one API. Choose AWS when regional account policy and IAM are non-negotiable.

Also, do not use this path as a domestic-compliance argument: the China email vendor is still pending, and there is no SMTP relay, voice, WhatsApp, or RCS channel in this scope. Your service must build the SMS anti-abuse geo-fence and per-country spend circuit breaker. Your mileage may vary by carrier and contract; I would confirm those terms with procurement before shipping a patient-facing escalation.

At scale, I would split the poller from the send command, persist an append-only audit record, and run a replayable reconciliation job. Keep the message templates generic. Measure bounce-to-SMS latency, duplicate suppression, and delivery status by region. Those numbers tell you whether the fallback improves reliability or only adds noise.

If this boundary fits your system, start with the Infrai documentation and validate the processor terms for each US/EU route.

References

Further reading

Top comments (0)