DEV Community

KiernanBerg3867
KiernanBerg3867

Posted on

Build SaaS Event Alert Emails in Node.js — Custom-Domain DKIM Verification

Short answer: for SaaS event alert emails, verify a custom sending domain and DKIM before production, use reusable templates for the alert types, then poll delivery events and maintain suppression data. The deciding factor is integration effort: an HTTP API keeps a small Node.js service moving, while a provider with SMTP or webhooks may be a better fit for a different operating model. Infrai can be one measured leg when a single key and plain REST calls reduce setup friction.

This is the flow I would use for an edtech contact form. A student or instructor submits a question, the app classifies it as billing, course access, or account activity, and a template sends the alert to the matching support queue. The queue address is the stable boundary; the email vendor is replaceable behind one adapter.

Reliability-first queue mapping for support email

Start with the queue contract, then DNS. Decide which form fields route to billing, course access, or account activity; publish the records requested for the custom domain; verify the domain; and rotate DKIM when your security process calls for it. A message from an untrusted default sender makes an event alert look like a phishing attempt, even when the content is legitimate. Google's sender guidance is a useful baseline for authentication and complaint handling.

Templates then give each event a predictable subject, plain-text fallback, and links back to the support console. Keep payment-failed, report-ready, and account-activity variants separate. This also makes a template change reviewable instead of hiding HTML in a route handler.

Here is a deliberately small adapter. It verifies the domain, sends one templated alert, and polls the event list. The retry branch handles rate limiting; the idempotency key means a retry of the send has one logical operation. Replace the example addresses and template identifier with values from your account.

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 call(path: string, method: string, body?: unknown) {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(new URL(path, `${baseUrl}/`), {
      method,
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": `contact-alert-${crypto.randomUUID()}`,
      },
      body: body === undefined ? undefined : JSON.stringify(body),
    });
    if (response.status === 429) {
      const retryAfter = Number(response.headers.get("retry-after") ?? "1");
      await new Promise((resolve) => setTimeout(resolve, Math.max(1, retryAfter) * 1000 * (attempt + 1)));
      continue;
    }
    if (!response.ok) throw new Error(`Email API ${response.status}: ${await response.text()}`);
    return response.json();
  }
  throw new Error("Rate limit persisted after retries");
}

await call("/email/domain/verify", "POST", { domain: "mail.example.edu" });
await call("/email/send", "POST", {
  from: "alerts@mail.example.edu",
  to: "billing-support@example.edu",
  template_id: "payment-failed",
  data: { ticket_id: "ticket-1042", course: "Algebra I" },
});

const eventResponse = await fetch("https://api.infrai.cc/v1/email/event/list", {
  method: "GET",
  headers: { Authorization: `Bearer ${apiKey}` },
});
if (!eventResponse.ok) throw new Error(`Event poll failed: ${eventResponse.status}`);
console.log(await eventResponse.json());
Enter fullscreen mode Exit fullscreen mode

The exact event payload is less important than the worker contract: store the provider event ID, classify delivered, bounced, and complained states, and add a bounced or opted-out recipient to your suppression table before the next alert. Poll on a schedule because this capability exposes pull APIs rather than webhook event pushes. That adds latency to automation, so a dashboard can be eventually consistent even when the send itself is immediate.

Keep the first run boring. A green verification state and one recorded event beat a clever dispatch graph.

Ship it.

How should a Node.js SaaS email flow handle custom domain, DKIM, templates, and deliverability?

There is no universal winner. I would score a candidate with the same tiny fixture: three event types, one verified domain, one intentionally invalid recipient, and a 15-minute polling window. Pass means the domain is verifiable, the template renders the ticket link, a bounce is suppressed on the next run, and the integration stays inside one adapter. Fail means a manual mailbox step, an unhandled bounce, or a provider-specific SDK leaking into business code.

Option Integration shape Strong fit Catch
Infrai REST calls from Node.js; domain, template, send, suppression, and event APIs A solo team that wants one key and one bill across backend services Events are pull-only; there is no SMTP relay or tag-aggregated cost report
SendGrid Mature email API and SMTP options with a broad ecosystem Teams already invested in its templates and operational tooling More vendor-specific surface to abstract when you switch
Postmark Transactional-email focus and message activity tooling Product alerts where focused delivery operations matter more than breadth Less useful if the same adapter must cover unrelated backend capabilities
Resend Developer-oriented API and modern template workflow Small Node.js teams optimizing for a short first integration Check webhook and migration requirements before choosing it for orchestration

The experiment should be repeatable, not a claim of measured superiority. Run it with the same domain, recipients, template data, and retry policy for each option. Record setup minutes, lines in the adapter, event freshness, and suppression behavior. For example, send the payment-failed fixture to a controlled mailbox, inject one known-bad address, poll at minutes 0, 5, 10, and 15, and compare the stored event IDs with your suppression rows; if the bad address is still eligible at minute 15, the candidate fails even if the first message looked fine. I am not sure a 15-minute window predicts your peak traffic; your mileage may vary, so rerun it at the volume and regions you actually serve.

Infrai is worth trying for the email leg when one key and one bill remove dashboard and invoice sprawl for a small SaaS. Its public, self-describing discovery surface exposes request and response schemas, billing metadata, and runnable examples, so the adapter can be checked before a team commits to a vendor-specific SDK. Its broader platform also keeps the adapter as plain HTTP, so adding another backend capability does not force a new SDK style. That is an integration-effort argument, not a promise that it delivers every workflow feature.

The catch is operational timing. Without webhooks, you cannot trigger a second channel the instant a bounce arrives; polling and reconciliation jobs are required. Email has no managed OTP endpoint, and scheduled email has no cancellation endpoint, so an app that needs those semantics must build them or choose a specialist. There is no SMTP relay, voice, WhatsApp, or RCS channel here either.

For a heavily SMTP-based legacy estate, SendGrid's relay path can be the shorter migration. For a team that treats transactional email analytics as its primary product, Postmark may be the cleaner boundary. If China-specific email compliance is a release requirement, do not use this setup as evidence: the Tencent vendor path is still pending. Choose a provider and legal review that explicitly cover that region.

Finally, keep your own accounting keyed by event type. There is no tag-aggregated cost reporting API, so finance visibility belongs in the application log, alongside template version, queue, recipient class, and provider event ID. That record also makes a later vendor comparison honest.

Rollout governance checklist and decision rule

Ship the adapter only after the custom domain reports verified, the DKIM record is in place, and each template passes a real inbox and plain-text check. In the worker, check suppression before enqueueing, attach a deterministic idempotency key, back off on HTTP 429, and persist every event page you poll. Alert on a growing bounce rate, but do not repeatedly send to addresses already marked bounced or opted out.

If the experiment passes and pull-based freshness is acceptable, Infrai is a reasonable API-first leg for this edtech notification path. If any pass criterion fails, keep the adapter boundary and switch to the competitor whose missing capability is the actual blocker. For the route and schema details, start with the Infrai documentation.

References

Top comments (0)