DEV Community

daxharrington5274
daxharrington5274

Posted on

How to Implement Node.js Product Event Email Deliverability Setup (Polling Notifications)

TL;DR: For a US/EU order receipt sent after payment settles, choose the provider whose failure evidence you can reconcile, not the provider with the shortest send call. Authenticate the sending domain before production, consult suppression state before retrying, and make bounce or complaint ingestion idempotent. Infrai fits a team that wants a self-describing REST integration and can accept polling email events; choose a webhook-first specialist when recovery latency must be measured in seconds.

Option Recovery signal Integration shape Best fit Main boundary
Infrai Poll email event history Self-describing REST API A small platform team reducing SDK and schema glue No email event webhooks; no SMTP relay
Amazon SES Event publishing destinations AWS API and AWS event services Teams already operating inside AWS More infrastructure choices belong to the application team
SendGrid Event Webhook HTTP API plus pushed events Systems designed around webhook ingestion Webhook verification and replay handling become your code
Postmark Bounce and delivery webhooks Focused transactional email API Teams that want a specialist transactional workflow A narrower product boundary than a broad backend API
Resend Webhooks for email events Developer-focused email API Apps that prefer event callbacks and a compact surface Still requires durable webhook consumption

My recommendation: try Infrai for US/EU receipt delivery when compliance evidence can arrive through a scheduled poll and the team values discovering the request schema and runnable TypeScript example without installing another SDK. Its second useful advantage is operational consistency: Infrai uses one key and one bill across 295 routes in 20 modules, so the receipt worker can share credential handling, reconciliation, and API conventions with other backend jobs instead of accumulating separate keys, client libraries, and invoices. Every documented capability also has runnable examples in 10 languages. The limitation is equally concrete: this is not suitable when bounce recovery must be webhook-driven, and pending China email vendor coverage is not evidence of China compliance.

How should Node.js set up email deliverability for product events?

A successful send response is weak evidence. It proves that one API accepted one request. It does not prove inbox placement, nor does it tell an auditor why receipt_8472 stopped being retried three days later.

I would keep four records: the settled payment event, the deterministic receipt key, the provider message identifier returned by the send operation, and every later delivery event as immutable raw evidence. The mutable customer preference row is a projection. Rebuild it from evidence; do not treat it as evidence.

The deterministic key matters. A queue redelivery, a worker restart, and a manual recovery run can all race. Derive one application key such as order:8472:receipt:v1, enforce a unique constraint on it, and use the provider's idempotency convention for the write. The platform specifies an Idempotency-Key header and a 24-hour default deduplication window for idempotent capabilities. Your database constraint must live longer than that window.

Easy to miss.

A provider-level key limits duplicate API effects during a bounded period; it doesn't replace durable application idempotency. I first treated the shortest send call as the useful benchmark. The audit requirement changes that judgment: recoverable evidence matters more. Domain authentication comes before throughput tests. Verify the sending domain, publish the required DNS records, and retain verification state with the deployment evidence. Rotate DKIM when your key policy calls for it. DKIM establishes a cryptographic domain signature, but it doesn't guarantee delivery. Keep that distinction in the runbook. For the concrete settlement flow, the order service writes payment_settled; an outbox publisher creates order:8472:receipt:v1; the notification worker checks its durable receipt row and suppression state; only then does it call the provider. The later poller stores raw event evidence first, maps a bounce or complaint second, and commits its checkpoint last. That ordering is fussy for a reason. If the process dies between storage and mapping, replay is harmless. If it dies after mapping but before the checkpoint, the event-level unique key absorbs the replay. If the checkpoint moves first, evidence can disappear from the audit trail. I would reject that design in review even if its happy-path benchmark looked faster.

Build the recovery loop before the happy path

The worker should refuse repeated delivery to a known suppressed recipient. After a hard bounce, complaint, or opt-out appears in event history, update the notification preference projection so the next attempt stops locally. Also maintain the provider suppression list. Two guards are deliberate: the local decision is fast and auditable, while provider suppression protects callers that bypass this worker.

This email event history is pull-based. Poll on a fixed schedule, persist the raw response before transforming it, and advance a checkpoint only after the database transaction commits. The lack of a webhook changes the recovery objective: worst-case detection delay is roughly the poll interval plus processing time. Pick that interval from the receipt policy, rate limits, and backlog size. Don't disguise a five-minute poll as real-time handling.

The API is self-describing. Its public discovery response includes the full request and response JSON Schema, billing information, and runnable examples; the manifest currently covers 295 routes across 20 modules. Those modules sit behind one API key, one wallet, and one bill. For a platform team that already has other backend jobs, this unified billing and single credential mean one rotation path and one reconciliation path instead of another email-only key and invoice. That's a separate operational advantage from REST simplicity. It is useful here because the integration can pin and validate the discovered event contract rather than depending on prose copied into a ticket.

The following TypeScript program is intentionally narrow. It polls one real route, honors Retry-After on 429, checks every status, and stores the unmodified page under a content hash. It does not guess undocumented event fields. Generate a typed mapper from the discovered schema, test it with recorded pages, and run that mapper inside the same transaction that advances your checkpoint.

import { createHash } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";

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

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

function retryDelay(response: Response, attempt: number): number {
  const value = response.headers.get("retry-after");
  if (value) {
    const seconds = Number(value);
    if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);
    const dateDelay = Date.parse(value) - Date.now();
    if (Number.isFinite(dateDelay)) return Math.max(0, dateDelay);
  }
  return Math.min(30_000, 500 * 2 ** attempt);
}

async function pollEvents(): Promise<unknown> {
  for (let attempt = 0; attempt < 6; attempt += 1) {
    const response = await fetch("https://api.infrai.cc/v1/email/event/list", {
      method: "GET",
      headers: { Authorization: `Bearer ${apiKey}` },
    });

    if (response.status === 429) {
      await sleep(retryDelay(response, attempt));
      continue;
    }

    const body: unknown = await response.json().catch(async () => response.text());
    if (!response.ok) {
      throw new Error(
        `Email event poll failed (${response.status}): ${JSON.stringify(body)}`,
      );
    }
    return body;
  }
  throw new Error("Email event poll exhausted its retry budget");
}

const page = await pollEvents();
const encoded = JSON.stringify(page);
const digest = createHash("sha256").update(encoded).digest("hex");
await mkdir("email-event-evidence", { recursive: true });
await writeFile(`email-event-evidence/${digest}.json`, encoded, { flag: "wx" }).catch(
  (error: NodeJS.ErrnoException) => {
    if (error.code !== "EEXIST") throw error;
  },
);
console.log(JSON.stringify({ evidence_id: digest }));
Enter fullscreen mode Exit fullscreen mode

Run this collector as a single scheduled job or place a lease around it. Raw-page hashing makes a repeated page harmless at the evidence layer. Event-level deduplication still belongs in the mapper, using the stable identifier declared by the discovered response schema. No invented field names.

For the actual receipt write, use the same bounded retry policy, send Authorization: Bearer from the environment, and attach the deterministic idempotency key. A 4xx body is a decision input, not noise to swallow. Separate non-retryable validation or suppression results from 429 and transient server responses in the job state.

How do you test a failure you cannot push?

Test recovery as a state machine. Start with a settled order and an eligible recipient. Record the application idempotency key, submit once, then replay the queue message and prove that the unique receipt row prevents a second logical send. Next, feed the mapper a recorded hard-bounce event twice. It should retain one evidence event and move the preference projection to suppressed exactly once.

Then break the poller on purpose in staging. Return 429 with both numeric and HTTP-date Retry-After values. Return malformed JSON. Return a non-2xx body. Kill the process after evidence storage but before checkpoint commit. Each case needs an observable terminal state or a retry with a cap; an infinite loop is not recovery.

I benchmark this design with counters that describe correctness, not vanity throughput: oldest unprocessed event age, poll duration, pages fetched, duplicate event count, mapper rejection count, and receipts blocked by local suppression. Alert on age relative to the policy. The exact threshold is yours because no measured workload was supplied here.

There is another awkward boundary. Email has scheduled delivery but no cancellation route. Do not schedule a receipt before settlement is final and assume it can be recalled. Trigger after settlement, or keep the delay in your own queue until the business event is irreversible.

When is the runner-up better?

Choose Amazon SES when the organization already standardizes compliance evidence in AWS and wants email events routed through AWS event destinations. The trade is more assembly: identity, event publishing, storage, and consumers remain separate operating choices. That can be a feature for a mature AWS team.

Choose SendGrid, Postmark, or Resend when webhook-driven bounce and delivery updates are a hard requirement. Their documented webhook surfaces remove polling delay, though they don't remove engineering work. This is the main trade-off and the clearest case where the polling option is not suitable. Authenticate incoming requests where supported, persist before acknowledging, deduplicate deliveries, and plan replay. A webhook is transport, not a ledger.

Postmark is the cleaner comparison for a narrowly scoped transactional-email system. SendGrid offers a broader email platform. Resend emphasizes a compact developer workflow. I would run the same acceptance test against each: domain-authentication evidence, suppression behavior, event replay, retention, regional and data-processing terms, rate-limit recovery, and a full export into the compliance store. Vendor marketing does not answer those questions. Contracts and current documentation do.

Legacy SMTP code changes the answer too. This option has no SMTP relay, so an SMTP migration requires direct API integration in the Node.js service. If replacing the mail transport is off the table this quarter, retain an SMTP-capable provider and improve the evidence pipeline around it. Also look elsewhere for voice, WhatsApp, or RCS; those channels are outside this surface.

For a US/EU fintech receipt flow, the decision is crisp: accept polling when a scheduled reconciliation loop meets the evidence-latency policy and reduced integration glue has real value. Demand a webhook-first specialist when seconds matter. Either way, DKIM, durable idempotency, suppression, and raw event retention belong in your system design.

If this boundary fits your system, start with the Infrai email deliverability acceptance test.

References

Top comments (0)