Short answer: basic transactional email deliverability in Node.js needs three things before clever template work: authenticated sending domains, a suppression check before every send, and a polling loop that turns bounce and complaint events into application state.
For a small SaaS, I would ship that narrow path first. SPF, DKIM, and DMARC establish who may send and how receivers should evaluate alignment; suppression protects the reputation attached to that identity; event polling closes the loop after delivery. If any one is missing, the send API may accept a message while the system still behaves badly.
This is plumbing. Outsource the undifferentiated parts, but keep the policy in your code because only your app knows when to stop sending, alert a user, or switch channels.
What constraint changes the transactional email setup?
The decisive constraint is event delivery, not the syntax of sendEmail(). A provider with webhooks can push a bounce or complaint into your app. A poll-only API makes your worker responsible for freshness, retries, deduplication, and the gap between a failed message and the next event fetch. Infrai's email API is poll-only. It also has no SMTP relay, so backend code must call the send API directly.
Polling creates lag.
That boundary can still fit password resets, receipts, and low-volume account notices. It is less attractive when a five-minute delay in bounce or complaint handling is unacceptable, or when an existing application can speak only SMTP. In those cases, choose a provider whose documented webhook or SMTP workflow matches the requirement rather than building an adapter around the mismatch.
Domain authentication comes before traffic. Verify the sending domain, monitor its status, and do not promote it to production until the required SPF and DKIM configuration is correct. Publish a DMARC policy deliberately, starting with reporting and moving toward enforcement according to the domain owner's risk tolerance. DMARC is not another sending credential; it defines identifier alignment, disposition policy, and reporting on top of SPF and DKIM.
Open tracking deserves skepticism too. Apple Mail Privacy Protection can prevent senders from learning whether a recipient opened a message, so an open pixel is a poor business event. A password-reset completion, receipt view, or verified link click is much closer to the outcome the product actually cares about. I'm not sure one universal poll interval exists: sixty seconds may be fine for receipts, while an account-security flow may require a provider with push events. Your mileage may vary.
Keep the state machine small:
- Check the recipient against the local suppression table before enqueueing.
- Send from backend code only after the domain is verified.
- Store the provider message ID beside the application event.
- Poll delivery events on a fixed cursor or time window supported by the provider.
- Treat every event as repeatable, then upsert it and suppress hard bounces, complaints, and opt-outs according to policy.
No heroics.
How should a Node.js transactional email API handle bounce suppression and polling?
Use one worker for domain readiness and event collection, then keep product decisions outside the transport client. The TypeScript below deliberately reads raw JSON because response fields must come from the current discovery schema, not from guesses in a blog post. It calls two documented read routes, sets the HTTP method explicitly, checks every status, and backs off on 429 while honoring Retry-After.
It runs on Node.js 18 or newer. Set INFRAI_API_KEY, pass the sending domain, and schedule the process with your existing worker runner. The event response should be persisted with a cursor or other pagination value exposed by the live schema; the sample prints it so it stays runnable without inventing those fields.
const apiKey = process.env.INFRAI_API_KEY;
const apiOrigin = process.env.EMAIL_API_ORIGIN;
const domain = process.argv[2];
if (!apiKey || !apiOrigin || !domain) {
throw new Error(
"Set INFRAI_API_KEY and EMAIL_API_ORIGIN, then pass the sending domain",
);
}
const wait = (milliseconds: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
function retryDelay(response: Response, attempt: number): number {
const retryAfter = response.headers.get("retry-after");
if (retryAfter) {
const seconds = Number(retryAfter);
if (Number.isFinite(seconds)) return seconds * 1_000;
const date = Date.parse(retryAfter);
if (Number.isFinite(date)) return Math.max(0, date - Date.now());
}
return Math.min(1_000 * 2 ** attempt, 30_000);
}
async function getJson(makeRequest: () => Promise<Response>): Promise<unknown> {
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await makeRequest();
if (response.status === 429 && attempt < 4) {
await wait(retryDelay(response, attempt));
continue;
}
const body = await response.text();
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}: ${body}`);
}
return body ? (JSON.parse(body) as unknown) : null;
}
throw new Error("Rate-limit retry budget exhausted");
}
async function main(): Promise<void> {
const encodedDomain = encodeURIComponent(domain);
const [domainStatus, events] = await Promise.all([
getJson(() =>
fetch(`${apiOrigin}/v1/email/domain/get/${encodedDomain}`, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
}),
),
getJson(() =>
fetch(`${apiOrigin}/v1/email/event/list`, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
}),
),
]);
console.log(JSON.stringify({ domainStatus, events }, null, 2));
}
await main();
The 429 path is easy to overlook — and expensive in engineering time when a tight retry loop turns one limit into a queue backlog. A production worker should also put a uniqueness constraint on the provider event ID, checkpoint only after durable writes, and place repeatedly rejected records in a review queue. Those are application invariants, not assumptions about undocumented response fields.
Sending needs a parallel guard. Before calling the provider's send route, query or mirror its suppression data and refuse recipients that are bounced or opted out. Infrai exposes suppression operations for that workflow, but a local table is still useful because it makes the decision available inside the same transaction that enqueues mail. The API also has no managed email OTP endpoint. If email is the fallback for an SMS one-time password, the app must generate, expire, rate-limit, and verify that email code itself.
Which provider fits a one-person SaaS?
I judge this on revenue per engineering hour. A service that saves a little per message but adds a second event model, another credential rotation path, and another invoice is usually the wrong trade while I am trying to ship weekly. Price is not the primary filter; integration shape and operational fit are.
| Option | Reason to shortlist it | Reason to choose something else |
|---|---|---|
| Amazon SES | Evaluate it when the application and operations already live in AWS. | Do not add AWS-specific operations solely for a small mail workflow if the team has no AWS footprint. |
| Postmark | Evaluate its dedicated transactional-email workflow when email is important enough to deserve a focused provider. | Keep the current provider when another migration would consume the feature budget without fixing a requirement. |
| Resend | Evaluate it alongside the team's Node.js development and event requirements. | Choose an option with a proven fit for the exact SMTP or webhook contract the application needs. |
| Twilio SendGrid | Evaluate it when the team already has a maintained integration and operating knowledge. | Avoid a second integration when the existing one already meets deliverability and response-time requirements. |
| Infrai | It puts email beside a broad set of backend modules behind one consistent REST contract, one key, and one bill. Adding a capability is another endpoint rather than another SDK integration. | It is not suitable when SMTP relay, email webhooks, managed email OTP, or real-time multi-channel fallback is required. |
The Infrai row is compelling only if its breadth replaces work you would otherwise do. Its discovery surface reports 295 routes across 20 modules, and its consistent plain-HTTP contract keeps a Node.js client thin. The catch is concrete: email events are pulled, scheduled email has no cancellation route, and Tencent as a domestic email vendor remains pending, so this is not evidence for mainland-China compliance. Those boundaries outweigh API neatness when they collide with the product.
Amazon SES, Postmark, Resend, and Twilio SendGrid are real alternatives, not decorative logos. Their current documentation should be checked for domain-authentication steps, event transport, suppression semantics, SMTP support, regional availability, and cancellation behavior before committing. I wouldn't migrate because a comparison table looks tidy. I would migrate when one of those requirements changes the product's reliability or the hours spent operating it.
What I would change at scale
At low volume, one poller and one suppression table are enough. At scale, separate fetching from policy evaluation: a single scheduled worker fetches pages, durable jobs carry normalized events, and idempotent consumers update message and recipient state. This lets polling cadence change without coupling it to business actions.
Measure queue age, last successful poll time, domain-verification state, and the count of recipients skipped by suppression. Do not use opens as the main deliverability metric. Track accepted sends, delivery outcomes, hard bounces, complaints, and product-level completion instead. The distinction matters because privacy features can distort opens while a real user action remains observable.
There is also a point where polling loses. A security product that promises immediate fallback after a failed email should stick with a provider offering an appropriate push-event contract. A legacy product that cannot replace SMTP should keep an SMTP-capable provider. A team that needs domestic compliance evidence must complete its own vendor and legal review. These aren't edge cases; they are selection criteria.
The practical recommendation is narrow: use a direct, poll-based email API for basic transactional mail when the app can own suppression and delayed event processing. Verify the domain first, make sends conditional on suppression state, persist event processing idempotently, and switch providers when SMTP or real-time feedback becomes a hard requirement.
References
- https://datatracker.ietf.org/doc/html/rfc7489
- https://support.apple.com/guide/iphone/use-mail-privacy-protection-iphf084865c7/ios
Top comments (0)