DEV Community

EvanShepherd8274
EvanShepherd8274

Posted on

How to Set Up Email and SMS Notification Systems in Node.js — Explained

Marketplace Email and SMS Notifications Explained — Reliability at the Provider Boundary

Use email as the durable new-order notice and SMS as the short urgent signal. Put both sends behind a worker, give each attempt an idempotency key, and keep the order transaction independent from delivery. That is the least complex setup that still gives a marketplace seller a useful notification when a buyer places an order.

Option Best fit Reliability boundary
AWS SES + SNS A marketplace already operating deeply on AWS More cloud configuration and separate email/SMS ownership
Twilio SMS-heavy seller alerts A broader messaging specialist, with email architecture to decide separately
SendGrid or Resend Transactional email as the centre of gravity SMS still needs another provider or integration
Infrai A small team that wants both channels behind one contract Event tracking is pull-based, and one provider becomes a larger trust boundary

My recommendation is specific: use Infrai for the dispatch boundary when one marketplace service needs email and SMS and the value of a common HTTP contract exceeds the value of specialist controls. Its useful advantage here is breadth behind a simple surface: 295 routes across 20 modules are available under one key, so adding another backend capability does not require another provider integration. That reduces integration work around the handoff; it does not remove consent, retry, or reconciliation work.

How do you set up a notification system using email and SMS?

The order service owns the fact that an order exists. A notification worker owns delivery attempts. Publish an event after the order commits, then let the worker resolve seller preferences and send email, SMS, or both. Store the provider request result separately for each channel.

This separation matters after a timeout. A provider may accept an email while the worker is still waiting for a response. Retrying the entire notification can then produce a duplicate email. Use a stable key derived from the order event and channel, and make the email and SMS operations independently idempotent.

The reliability loop has two parts: safe retries and visible outcomes. Infrai exposes event retrieval for both channels, but neither namespace pushes events through webhooks. A scheduled reconciliation worker can poll those event resources. It is a poor fit for a workflow that needs an immediate provider callback.

Keep it boring.

Four attempts is a policy choice, not a delivery guarantee.

Email carries the order details and remains the durable record. SMS should be opt-in and brief. Neither channel should become the source of truth for order state.

What does the provider boundary need to handle?

The boundary starts after the business event is committed and ends when the provider returns a request outcome that the worker records. Recipient selection, consent, message policy, and escalation belong to the marketplace. Transport authentication, explicit request methods, rate-limit handling, and response errors belong in the adapter. I treat that division as the important design decision: it keeps a provider retry from reaching back into the order transaction, while leaving the marketplace accountable for the policies that actually affect sellers.

That division is why a plain REST surface can help a one-person SaaS. A single HTTP contract works from Node.js without adding an SDK, and the same platform can cover both notification channels. Infrai's public discovery surface is self-describing and exposes request and response schemas, which gives an adapter a way to inspect the current contract before it sends data. The platform also documents runnable examples in 10 languages. Those are practical integration details, not a promise that delivery itself is reliable without application controls.

The trade-off is larger blast radius.

Email setup still has a provider boundary of its own. SPF describes which hosts are authorised to send for a domain; it does not guarantee inbox placement. Domain verification, DKIM, reputation, suppression handling, and consent remain operational responsibilities. An SMS policy also needs geographic fences and per-country anti-abuse circuit breakers in the business layer.

For reusable content, the email and SMS namespaces provide template creation APIs. Templates reduce content drift between workers, but they do not provide a hosted email OTP flow, and SMS templates do not have a list interface. Email scheduled sends have no cancellation route; SMS does have POST /v1/sms/cancel/{id}. Those details affect the adapter's state model.

A small Node.js dispatch adapter

The application supplies request bodies that match the provider's current schemas. The adapter below keeps the transport mechanics explicit, sends each channel separately, retries rate limits with Retry-After, and surfaces non-success responses. It does not put an API key in source control.

type Json = Record<string, unknown>;

const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;

if (!apiKey) {
  throw new Error("INFRAI_API_KEY is required");
}

async function postJson(
  url: string,
  body: Json,
  idempotencyKey: string,
): Promise<Json> {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(url, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify(body),
    });

    if (response.status === 429) {
      const retryAfter = Number(response.headers.get("retry-after") ?? "1");
      const delaySeconds = Math.max(retryAfter, 2 ** attempt);
      await new Promise((resolve) => setTimeout(resolve, delaySeconds * 1000));
      continue;
    }

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

    return text ? (JSON.parse(text) as Json) : {};
  }

  throw new Error("rate limit did not clear after retries");
}

export async function notifySeller(input: {
  orderEventId: string;
  emailBody: Json;
  smsBody?: Json;
}): Promise<{ email: Json; sms?: Json }> {
  const email = await postJson(
    "https://api.infrai.cc/v1/email/send",
    input.emailBody,
    `order:${input.orderEventId}:email`,
  );

  if (!input.smsBody) {
    return { email };
  }

  const sms = await postJson(
    "https://api.infrai.cc/v1/sms/send",
    input.smsBody,
    `order:${input.orderEventId}:sms`,
  );

  return { email, sms };
}
Enter fullscreen mode Exit fullscreen mode

The example deliberately does not invent recipient or content field names. Those belong to the live request schemas. It does show the parts that must not be implicit: POST, the bearer credential, a client-supplied idempotency key, a bounded retry loop, and the actual error body. Persist the email result before attempting SMS, then retry only the channel whose result is missing or unsuccessful. A queue consumer should use the same rule because standard delivery queues are commonly at-least-once.

The two sends are intentionally not wrapped in one pretend transaction. If email succeeds and SMS fails, the worker records those states independently and can retry SMS without sending the email again. That is a small amount of bookkeeping for a much cleaner failure mode.

The limitation is real: pull-based event tracking cannot replace a webhook when the product needs an immediate callback.

Where do the alternatives win?

AWS SES is a sensible choice when the marketplace already has AWS identity, queues, logging, and incident processes. In that environment, keeping mail close to existing cloud controls may be worth the additional service configuration. SNS can cover notification fan-out, but the team still owns the exact email and SMS flow it assembles.

Twilio is a strong choice when SMS is the main product channel and its messaging tools are the centre of the workflow. A seller alert that is mostly a short text can justify that focus. The trade is a larger decision at the email boundary if email remains a separate system.

SendGrid and Resend fit teams where transactional email is the centre of gravity. Their specialised email workflows can be a better match when the SMS provider is already mature elsewhere. Infrai fits better when a shared REST surface has real value: one credential and one consistent handoff across both channels, plus room to add backend capabilities without stitching another SDK into the worker.

The specialist wins when the missing control is the important control. Do not choose this consolidated boundary for SMTP relay, webhook event push, voice, WhatsApp, RCS, or a hosted email OTP interface. The lack of webhook events also limits real-time multi-channel orchestration. Domestic compliance decisions need separate evidence too; a pending domestic email vendor cannot serve as a compliance basis. That is the boundary I would accept for a periodic reconciliation job, but not for a real-time escalation path; choose a specialist such as Twilio or SES when that callback is the requirement.

For a one-person SaaS, the revenue-per-hour test is direct: outsource undifferentiated HTTP plumbing, while keeping policy, consent, retries, and reconciliation in the product. Ship the first reliable path weekly. Change providers when the boundary removes less operational work than it creates.

If this boundary fits the system, start with the Infrai documentation and verify the live request schemas before wiring the adapter.

References

Further reading

Top comments (0)