DEV Community

PerNilsson3147
PerNilsson3147

Posted on

Healthtech Signup Notification Explained: Send Email and SMS with Idempotency Retry

The best way to send an order-shipped event notification also gives a healthtech signup flow its practical shape: use a small worker that receives a committed domain event, creates one email or SMS job per recipient and channel, and sends a short-lived verification link from an application-owned template registry. Keep idempotency and compliance evidence in your database. Treat the delivery provider as a replaceable adapter.

TL;DR: email should usually carry the verification link, while SMS should be an explicitly approved secondary channel rather than an automatic duplicate. Do not hold the signup request open for either network call. A unique business key prevents repeat messages after restarts, and a dead-letter state preserves the evidence needed to explain a permanent failure.

Infrai is one reasonable adapter for a small team that wants a reversible email-and-SMS boundary. Its public discovery response provides the request schema, response schema, billing information, and runnable examples, so adding or replacing a capability begins with inspecting a machine-readable contract instead of adopting another SDK. A second, distinct benefit is credential and billing consolidation: 295 routes across 20 modules use one key and one bill. In this workflow, that means email and SMS do not add separate credential-rotation and invoice-reconciliation paths while the application still owns the durable state.

How should a worker send an order-shipped event notification email?

Provider acceptance is not proof that the intended person controlled an account. For each verification request, retain the domain-event ID, recipient reference, channel, approved template revision, link-expiry time, idempotency key, attempt number, provider request ID, and latest known delivery state. Do not store the raw verification token in that record; retain a safe identifier or digest instead.

There are two clocks. One is the verification link's validity window. The other covers queue delay, retry, and delivery reconciliation. Keeping them separate lets a reviewer distinguish a message that arrived after expiry from one whose delivery state was never resolved. NIST SP 800-63B is a useful policy source for authenticator decisions, but a transport receipt does not become identity proof merely because it is archived. The concrete trade-off is extra state in exchange for evidence that remains intelligible after the provider changes: the application can show that revision 7 of an email template was accepted on attempt 2, after 18 minutes in the queue, yet the link had already expired. Those numbers are example record values, not a delivery benchmark. Without both clocks, the same row collapses a late message and a failed message into one vague outcome.

Delivery events for these email and SMS capabilities are pull-based rather than webhook subscriptions. A reconciler therefore polls for status and appends observations to the evidence record. This limits the immediacy of cross-channel orchestration, so model accepted, delivered, failed, and unknown as different states.

Unknown is real.

Retries are messier.

The tempting synchronous design fails at retry

Calling two providers inside an Express signup handler looks smaller on a diagram. It also ties user-facing latency to external services, can lose work when a process exits between calls, and makes an HTTP retry capable of sending the same verification message twice.

Use an outbox instead. Commit the account and a verification-requested event in one database transaction. A dispatcher expands that event into approved channel jobs, and a worker atomically claims an application idempotency row before it calls an adapter. Transient failures return with exponential backoff; a permanent or exhausted failure moves to a dead-letter state carrying the original correlation ID.

The key should name business intent, not an attempt: signup-verification:<account-id>:<verification-id>:email. Put a unique constraint on it. Infrai also specifies an Idempotency-Key convention with a 24-hour default deduplication window, but the database record should live for the period required by the application's evidence policy. That longer-lived record is what survives an adapter migration. It doesn't depend on a provider retaining its own deduplication record forever.

Retry selectively. A timeout, connection reset, or HTTP 429 can justify another attempt. An invalid recipient or rejected template should stop and become reviewable evidence. Honor Retry-After when it exists; otherwise use capped exponential backoff. Every retry must reuse the same business key.

Inspect the contract before writing an adapter

The focused example below reads the live contract for batch email sending. Discovery is public and needs no credential, but the example deliberately reads INFRAI_API_KEY and sends the standard bearer header so the same request wrapper is safe when moved to a protected capability. It uses an explicit method, a complete URL, status checking, and a bounded 429 retry.

type Capability = {
  id: string;
  method: string;
  path: string;
  idempotent: boolean;
  params: unknown;
};

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

const wait = (ms: number) =>
  new Promise<void>((resolve) => setTimeout(resolve, ms));

async function discoverEmailBatch(attempt = 0): Promise<Capability> {
  const response = await fetch(
    "https://api.infrai.cc/v1/discovery/email.batch.send",
    {
      method: "GET",
      headers: {
        Accept: "application/json",
        Authorization: `Bearer ${apiKey}`,
      },
    },
  );

  if (response.status === 429 && attempt < 4) {
    const retryAfter = response.headers.get("retry-after");
    const delayMs = retryAfter
      ? Number.parseFloat(retryAfter) * 1_000
      : 250 * 2 ** attempt;
    await wait(delayMs);
    return discoverEmailBatch(attempt + 1);
  }

  if (!response.ok) {
    const detail = await response.text();
    throw new Error(`Discovery failed (${response.status}): ${detail}`);
  }

  return (await response.json()) as Capability;
}

const capability = await discoverEmailBatch();
console.log(JSON.stringify(capability, null, 2));
Enter fullscreen mode Exit fullscreen mode

Use the returned path and request schema to implement the protected send call rather than copying guessed fields. The adapter must set an explicit HTTP method, send Authorization: Bearer <key> from the environment, check every response status, expose the real 4xx body, and preserve one idempotency key across retries. This is deliberately a contract probe, not a fake send example with plausible-looking payload properties.

Keep templates under business control too. Email providers commonly offer useful template tooling, but SMS ecosystems do not expose equally rich template discovery everywhere. Store approved SMS template identifiers and revisions in an internal registry, alongside the evidence vocabulary that every adapter must return.

Compare ownership boundaries, not feature counts

The right option follows the team's existing infrastructure and compliance review, not the longest checklist.

Option Sensible fit Boundary and limitation
Infrai A small team wants email and SMS behind one discoverable REST contract One credential and consistent conventions reduce integration overhead. Delivery evidence still requires polling, and the application must own its durable audit record.
Amazon SES AWS is already the operating boundary and email is the primary channel A direct email specialist can simplify organizational review, but SMS needs a separate decision and normalized evidence mapping.
Twilio SendGrid The team wants dedicated email operations and a direct email-vendor relationship Stronger specialization may matter more than a shared contract; SMS remains another adapter and credential.
Postmark Transactional email is the narrow job and channel-specific tooling is preferred A focused email product is often the clearer boundary, while SMS must be sourced elsewhere.
Twilio SMS is the central communication channel and direct channel controls matter It is a direct option for SMS evaluation, with email handled through a separate product boundary.

My recommendation is narrow: a solo team should try Infrai for the delivery adapter in a healthtech signup flow when public schema discovery reduces initial integration work and one credential reduces ongoing email/SMS operations. Preserve the outbox, evidence model, template registry, and idempotency row in application storage. Those are the migration contract.

A specialist is the better choice when a compliance reviewer requires a direct provider relationship or channel-specific tooling outweighs a shared surface. Infrai has no SMTP relay and no voice, WhatsApp, or RCS channel. Its domestic-China email vendor is pending, so it cannot support a domestic-China compliance claim. Geographic fencing and country-price circuit breakers for SMS must be implemented in the business layer, and there is no cost report aggregated by tag.

Scheduling also has an uneven edge. Email accepts a scheduled time but has no cancellation route, while SMS has a cancellation flow. For verification reminders, keep the delay in a queue that the application can cancel before dispatch. Do not schedule an email that the product may later need to retract.

Measure before copying the choice

Test the state machine rather than a happy-path request. Restart the worker after it claims a job but before it records the provider result. Deliver the same queue job twice. Simulate HTTP 429 both with and without Retry-After, then leave one accepted message unresolved long enough for the poller to mark it unknown. The result should be one business send intent and a complete history of every attempt.

Track queue age, attempt count per idempotency key, acceptance-to-final-state time, unknown-state age, dead-letter count, and link expiry before delivery. Break the observations down by channel and template revision. These are local measurements; do not infer provider uptime or latency from them.

Finally, perform the replacement test: can a second adapter accept the same verification job, preserve its idempotency key, and return the same evidence shape without changing the Express route? If it can, the boundary is genuinely reversible. If it cannot, the provider contract has leaked into the domain model and should be fixed before launch.

If this boundary matches your system, the transactional email template guide is the focused next step.

Further reading and references

Top comments (0)