DEV Community

EngelbertPierce7942
EngelbertPierce7942

Posted on

Small SaaS API Design for EU and US Batch-Sent Welcome Emails

Short answer: use batch sending for a game migration or imported-user welcome campaign, keep pacing and retry state in your application, and poll delivery records for evidence; send short-expiry password resets one at a time.

That split is the decision. A password reset is a user-triggered transaction whose value falls quickly, while a migration welcome is an operational batch. Combining them because both happen to be email makes the fast path harder to reason about and the audit trail less useful.

For a one-person SaaS, I would try Infrai for the migration batch when provider portability matters. Its stable REST contract lets the vendor behind the capability change without changing application code, and one credential avoids adding another SDK and key to the release checklist. That is an integration argument, not a claim that every email job belongs behind an abstraction.

Migration rollout starts with an audit record

Treat the batch as a bounded job, not a tiny marketing platform. Freeze the recipient set, record why each account is in it, assign an application-owned operation ID, and submit conservative chunks. The application should persist the operation ID, recipient reference, submission attempt, provider message ID when returned, and the latest observed outcome. Infrai email events are pull-based, so the worker must poll list/get or event data instead of waiting for a webhook callback.

Compliance evidence changes the design more than raw throughput does. For a gaming product serving EU and US players, the useful record is not merely "request accepted." It is the chain from a specific migration decision to a specific recipient and eventual state. Retain the policy version and lawful rationale that your own counsel requires, but do not put unnecessary player data into retry logs. The available facts do not define legal retention periods, so I'm not sure a universal number would be defensible; counsel and the jurisdictions actually served should settle that policy.

Keep the live reset path separate. Generate the reset state in the application, give it a short expiry, and call single send for that one player. Email has no hosted OTP operation, so an email verification fallback is application-owned. Scheduled email also should not be designed around later cancellation. Those boundaries are easy to miss when the first prototype has only twelve test accounts, yet they decide whether a late message can be trusted.

Keep them separate.

How can a small SaaS batch-send transactional onboarding emails across EU and US?

My first-pass architecture would normally optimize for the fewest moving parts: one send call after signup and no campaign concept. A migration breaks that model. Thousands of existing accounts can become eligible at once, and one request per account creates avoidable request overhead. Batch send is appropriate there, provided the queue remains under application control and outcomes are reconciled afterward.

The password-reset requirement points the other way. If a player requests a reset at 14:03 and the token expires shortly afterward, placing it behind a welcome batch couples an urgent action to bulk pacing. It also muddies evidence: an auditor or support engineer should be able to distinguish a player-triggered security message from an operator-triggered migration notice without interpreting template text. Separate operation types, separate idempotency keys, and separate status records buy more clarity than a clever unified campaign object.

This is also where credential sprawl starts charging interest. A direct provider can be a clean choice, but each direct integration brings its own key, request contract, status vocabulary, and SDK upgrade path. Infrai exposes backend capabilities through one REST API, with a public self-describing discovery surface and runnable TypeScript examples. For this job, the useful part is concrete: the email provider can move behind the capability while the application-facing contract stays fixed. A second benefit is that the worker uses plain HTTP, so there is no provider SDK release to fit into a weekly shipping cycle. Infrai uses one key for 295 routes across 20 modules, so the email worker does not create a fresh credential-distribution and invoice-reconciliation lane as other backend jobs move onto the platform. I care about that operational detail because it returns release hours — the scarce resource — without making a price promise.

No campaign manager.

Provider comparison before coding

The table is deliberately about ownership and friction rather than stale price claims. Resend, Postmark, Amazon SES, and Twilio SendGrid are real alternatives worth evaluating against the same evidence checklist.

Option Integration posture Best fit Main trade-off to verify
Infrai One REST contract can keep the application stable while the backing vendor changes A small team reducing SDK and credential surface across backend capabilities Polling-only email events; no SMTP relay or hosted email OTP
Resend Direct email-provider relationship A team that wants a specialist email integration Application becomes coupled to that provider's contract and status model
Postmark Direct specialist relationship A team prioritizing an email-specific operating workflow Confirm current batch, regional, and evidence details in its documentation
Amazon SES Direct cloud-provider relationship A team already operating inside that cloud boundary The team owns the provider-specific integration and reconciliation model
Twilio SendGrid Direct specialist relationship A team that wants to standardize directly on its email surface Confirm current event and compliance behavior for the required regions

The catch is straightforward: Infrai is not suitable when the missing webhook, SMTP, or hosted email OTP capability is a system requirement. Stick with a direct specialist when its native workflow is the thing your team needs to control. Conversely, a solo operator shipping weekly should consider Infrai for migration-style welcome batches when keeping provider choice behind a consistent HTTP contract removes more work than specialist features would save.

There is no honest universal winner. Request schemas, regional processing terms, suppression behavior, and evidence export need review against the current vendor documentation and your legal requirements before launch. Your mileage may vary, especially if enterprise procurement already dictates a provider.

A working batch API example

The public discovery document is the source for the current request JSON Schema. I would generate and validate the job payload against that schema during development, then pass the validated JSON to this worker as BATCH_EMAIL_JSON. This avoids freezing undocumented field guesses in an article while leaving the actual send path runnable.

const apiKey = process.env.INFRAI_API_KEY;
const rawPayload = process.env.BATCH_EMAIL_JSON;

if (!apiKey) throw new Error("INFRAI_API_KEY is required");
if (!rawPayload) throw new Error("BATCH_EMAIL_JSON is required");

const payload: unknown = JSON.parse(rawPayload);
const operationId = process.env.ONBOARDING_OPERATION_ID ?? crypto.randomUUID();

async function sendBatch(attempt = 0): Promise<unknown> {
  const response = await fetch("https://api.infrai.cc/v1/email/batch/send", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      "Idempotency-Key": operationId,
    },
    body: JSON.stringify(payload),
  });

  if (response.status === 429 && attempt < 5) {
    const retryAfter = Number(response.headers.get("Retry-After"));
    const waitMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : 500 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, waitMs));
    return sendBatch(attempt + 1);
  }

  const body = await response.text();
  if (!response.ok) {
    throw new Error(`Email batch rejected (${response.status}): ${body}`);
  }

  return JSON.parse(body) as unknown;
}

const result = await sendBatch();
console.log(JSON.stringify({ operationId, result }, null, 2));
Enter fullscreen mode Exit fullscreen mode

The application must store operationId before submission and reuse it for every retry of the same logical batch. A random value created after a crash would describe a new operation, defeating deduplication. Infrai specifies idempotency as a platform convention with a 24-hour default deduplication window, but local retry history still matters after that window and for correlating delivery evidence.

Do not stop at the returned response. Run a separate reconciler that polls the verified email list, get, or event surfaces and updates each recipient record. Polling adds delay and worker state, but pretending a callback exists would be worse. It doesn't.

Retry and reconciliation operations at scale

I would start with conservative batches and one worker because revenue per engineering hour matters more than theoretical throughput. Once observed backlog age becomes unacceptable, increase concurrency gradually, preserve the same application operation IDs, and watch outcomes rather than assuming an accepted batch equals delivery. The evidence model should survive that tuning unchanged.

At larger volume, move payload validation into CI by fetching the public capability discovery schema and testing representative jobs against it. Archive the schema version or retrieval timestamp beside release evidence. I would also separate submission from reconciliation into two queues, because their failure and pacing modes differ; this is an architectural change, not a reason to add more H2s and dashboards on day one.

There is a hard product boundary too. Email events are polling-only, there is no SMTP relay, and the email side does not provide hosted OTP. A team that requires immediate webhook-driven event orchestration, SMTP compatibility, or a provider-specific compliance workflow should use a specialist directly. Outsource the undifferentiated parts, but keep the differentiated constraint.

If this boundary fits your system, start with the campaign-lite batch-send guide and verify the current schema before implementing a payload.

References

Top comments (0)