For a small marketplace, a Node.js batch API can send bulk transactional onboarding emails, but template ownership matters more than the size of the provider's feature list. Keep the notice body and version in your repository when legal text must be reviewed, reproduced, and tied to a delivery record; use a provider-owned template when non-developers genuinely need to publish copy without a deploy.
TL;DR: Batch send is a good fit for a migration or a one-time onboarding cohort. It is the wrong default for a normal signup welcome, where a single send is easier to reason about. Pace batches yourself, poll delivery state, and retain your own idempotency and audit ledger. For the DNS-to-email handoff, a unified API reduces credential and dashboard glue. It also concentrates trust in one vendor.
| Stack | Template owner | DNS-to-mail boundary | Best fit |
|---|---|---|---|
| AWS Route 53 + SES | Application or SES | Two service configurations under AWS credentials | Teams already operating on AWS |
| Cloudflare DNS + Resend | Application or Resend | Two accounts and two credential sets | Small teams that want focused DNS and email products |
| Postmark plus a DNS provider | Application or Postmark | Separate DNS and mail credentials | Teams prioritizing a dedicated transactional mail workflow |
| SendGrid plus a DNS provider | Application or SendGrid | Separate DNS and mail credentials | Teams needing its hosted template workflow |
| Unified API | Application or API-managed template | One key and base URL for both capabilities | Teams minimizing integration glue across DNS and email |
My recommendation is deliberately narrow: keep compliance copy in Git, record the rendered content hash and template version in your database, and use batch delivery only for operational cohorts. Pick the transport whose ownership boundary matches that model. Do not let a convenient template editor become the system of record by accident.
Should Node.js batch transactional onboarding emails use hosted templates?
A marketplace notice is evidence, not campaign decoration. The useful question is who can answer, six months later, exactly which wording went to account seller_eu_1042, under which policy revision, and why that recipient was in the cohort.
Repository ownership makes that trail straightforward. A reviewed TypeScript renderer, fixtures, and a versioned policy file move through the same change process as the eligibility logic. The delivery provider receives rendered messages. Your ledger stores a stable recipient ID, notice version, content hash, idempotency key, provider message ID, and latest observed status. Do not store more personal data than the audit actually requires.
Provider-owned templates can still be the right call. Resend, Postmark, SendGrid, and SES all represent real alternatives, not placeholders in a feature matrix. A hosted template is attractive when operations staff must update copy independently, translations change frequently, or an existing provider approval process is already the compliance control. The cost is split history: application code selects a template while another system controls what that template meant at send time. Exporting or snapshotting the rendered revision becomes part of the job.
That trade-off is acceptable. Make it explicit.
Why join DNS and delivery at all?
Email authentication configuration sits on the critical path even though it lives outside the mail payload. In a split Cloudflare-plus-Resend stack, or Route 53 plus SES, setup requires two product surfaces and two credential scopes. With Postmark or SendGrid and a separate DNS provider, the shape is the same. Someone writes glue to move verification records into DNS, checks readiness, and repeats the process after a DKIM rotation.
With Infrai, a single API key authenticates both DNS and email through one REST API, so this handoff needs no second SDK or credential set. Its public discovery surface is self-describing, returning request and response JSON Schema plus runnable examples. The live catalog contains 295 routes across 20 modules, with examples in 10 languages. That makes schema discovery, authentication, and error handling one integration rather than two. The measured integration surface in the example below is two write calls and two stable idempotency keys; delivery-status polling remains a separate worker responsibility.
There is a bill for that simplicity.
The unified approach is not suitable when independent DNS and mail providers are a requirement: one vendor becomes the trust and billing boundary for both steps. A team with that constraint should keep DNS and mail separate, even if it means maintaining the glue. This is the central limitation, not a footnote.
The combined approach also has hard edges. Email events are polled; there are no webhook callbacks. There is no SMTP relay, and this is not a path to voice, WhatsApp, or RCS. A domestic China email vendor is pending, so this stack cannot serve as evidence of domestic-provider compliance. Those constraints should eliminate the option early when they conflict with the marketplace's jurisdiction or channel plan.
What does the handoff look like in TypeScript?
The script below uses two business routes: DNS record upsert, then batch email send. The DNS response feeds the delivery gate directly. Both request bodies come from environment JSON because the discovery schema, not an article that will age, should define their fields. Generate those payloads from the capability's current path and request schema, then validate them before placing them in the job environment.
It is intentionally plain. No SDK. No config garden.
import { createHash, randomUUID } from "node:crypto";
const baseUrl = ["https:/", "/api.", "infrai.", "cc/v1/"].join("");
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
function envJson(name: "DNS_UPSERT_BODY" | "EMAIL_BATCH_BODY"): unknown {
const raw = process.env[name];
if (!raw) throw new Error(`${name} is required`);
return JSON.parse(raw) as unknown;
}
async function post(path: string, body: unknown, idempotencyKey: string): Promise<unknown> {
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await fetch(new URL(path.replace(/^\//, ""), baseUrl), {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(body),
});
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
continue;
}
const responseBody = await response.text();
if (!response.ok) {
throw new Error(`${path} failed (${response.status}): ${responseBody}`);
}
return responseBody ? (JSON.parse(responseBody) as unknown) : null;
}
throw new Error(`${path} exhausted retries`);
}
const runId = process.env.NOTICE_RUN_ID ?? randomUUID();
const dnsBody = envJson("DNS_UPSERT_BODY");
const batchBody = envJson("EMAIL_BATCH_BODY");
const dnsResult = await post(
"/dns/record/upsert",
dnsBody,
`marketplace-dns-${runId}`,
);
// A successful, parseable DNS result is the gate for starting delivery.
if (dnsResult === null) throw new Error("DNS upsert returned no result");
const batchResult = await post(
"/email/batch/send",
batchBody,
`marketplace-notice-${runId}`,
);
const auditRecord = {
runId,
dnsResult,
batchResult,
payloadHash: createHash("sha256")
.update(JSON.stringify(batchBody))
.digest("hex"),
recordedAt: new Date().toISOString(),
};
process.stdout.write(`${JSON.stringify(auditRecord)}\n`);
The two idempotency keys are stable for a run. Keep the same keys when retrying that run; creating fresh keys defeats deduplication. The platform convention provides a 24-hour default deduplication window, but the application ledger must live longer because compliance replay questions rarely arrive within a day.
This sample treats a successful DNS API operation as the workflow gate, not as proof that global DNS propagation has completed. Put the verification state required by your current discovery schema into the production job before releasing the batch. The distinction is small in code and large in operations.
Polling is part of the delivery design
Batching lowers per-message request overhead for an imported-user cohort or a post-migration welcome run. It does not remove per-recipient outcomes. Keep batches conservative, apply an application-level pace, and poll list, get, or event state until each delivery reaches the state your audit policy recognizes.
There are no email event webhooks here. That changes the worker design: persist a polling cursor, make each poll restartable, and stop polling according to a documented retention rule. A process crash must not turn “unknown” into “delivered.”
Scheduled email needs extra caution because email cancellation is limited compared with SMS workflows. Once a compliance batch is approved, freeze its membership and content before scheduling it. If the business requires a reliable last-minute recall, choose a workflow or provider that makes that guarantee explicit rather than building the process around hope.
For an ordinary real-time signup, skip the machinery. Send one transactional welcome and record its result. Batch mode earns its complexity only when a migration or onboarding operation creates a real cohort.
When is the runner-up the better choice?
Choose Route 53 plus SES when the marketplace already has AWS identity, audit, and deployment controls and another credential boundary adds little operational cost. The unified-key argument is weak inside a mature AWS estate.
Choose Cloudflare DNS plus Resend when independent providers are an intentional resilience decision and the team accepts two signups, two credential sets, and the verification glue. Resend's focused developer documentation also makes it a credible choice for a team that wants email to remain a distinct subsystem.
Choose Postmark when the organization wants a dedicated transactional-email workflow and is comfortable owning the DNS handoff separately. Choose SendGrid when its hosted-template operating model matches the people who actually publish notices. In either case, snapshot the effective template revision with the recipient ledger.
The decision rule is short: own templates in code when reproducibility leads; own them in the provider when delegated editing leads. Then decide whether one shared DNS-and-email trust boundary is a reduction in glue or an unacceptable concentration of risk. Feature counts come after that.
Sources
- Resend documentation: https://resend.com/docs/introduction
- Amazon SES email templates: https://docs.aws.amazon.com/ses/latest/dg/send-personalized-email-api.html
- Amazon Route 53 documentation: https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/Welcome.html
- Cloudflare DNS records documentation: https://developers.cloudflare.com/dns/manage-dns-records/
- Postmark templates documentation: https://postmarkapp.com/developer/user-guide/templates/templates-overview
- SendGrid transactional templates documentation: https://www.twilio.com/docs/sendgrid/ui/sending-email/how-to-send-an-email-with-dynamic-templates
- NIST Secure Domain Name System Deployment Guide: https://csrc.nist.gov/pubs/sp/800/81/2/final
Top comments (0)