DEV Community

NoahHayes7250
NoahHayes7250

Posted on

Node.js Welcome Emails: A Dedicated-Domain Warmup Plan with API Monitoring

Short answer: warm a dedicated domain with low-volume welcome emails, raise the sending allowance gradually, and keep the allowance plus bounce and complaint outcomes in your own database.

For a one-person B2B SaaS, the least complex useful setup is a provider-owned template called through an API, guarded by an application-side daily cap. The provider delivers the verification link; the application owns the ramp and decides whether tomorrow's volume may rise. Don't automate an increase until you have reviewed the outcomes.

Choice Template owner Feedback path Best fit Main trade-off
Infrai Provider Poll events A plain REST integration without another SDK Slower feedback than a webhook flow
Postmark Provider Webhook-oriented option Fast event-driven reactions matter Another vendor-specific integration
Resend Provider Webhook-oriented option The team wants a focused email product Another vendor-specific integration
SendGrid Provider Webhook-oriented option Existing SendGrid operations should stay put More migration work if adopted only for signup mail
Amazon SES Application or provider workflow Provider-specific event plumbing The product already runs its mail operations on AWS More application ownership

Recommendation: use a provider-owned template and an application-owned ramp. Infrai is one reasonable fit when the priority is plain HTTP with no SDK or client-library version to maintain, while a single API key and one consolidated bill cover 295 routes across 20 modules. Its public discovery response supplies the request schema and runnable TypeScript example, so a solo operator can validate the contract before handling a key; when signup later needs an adjacent service, that avoids creating another credential rotation and invoice-reconciliation job. Stick with Postmark, Resend, SendGrid, or Amazon SES when its existing event workflow and operational knowledge are already part of your stack.

Governance starts in the warmup ledger

Monitor attempted sends, accepted sends, bounces, and complaints per dedicated domain and per day. Store those counters beside the current allowance. Infrai has no tag-aggregated cost or deliverability reporting API, so the application database is the durable decision record; event polling is a review input, not the ramp controller itself.

This split matters during signup. A verification message is small and predictable, but demand isn't. A mention in a newsletter can turn 18 signups on Monday into 140 on Tuesday. The warmup guard should admit only the day's approved volume, leave the rest queued, and never interpret a traffic spike as permission to accelerate. Exact thresholds are not universal. I'm not sure any fixed calendar can be defended without the domain's own outcomes, because reputation evidence is the missing input.

Keep it boring.

Use low-volume transactional traffic first: welcome links and password resets. Increase by day or week only after reviewing bounce and complaint outcomes. SPF is part of authenticating the sending domain, but a valid SPF record doesn't replace this feedback loop.

Integration workflow: put the call behind the volume gate

The API transport belongs behind the allowance check, not inside the logic that decides tomorrow's cap. The example below is runnable with Node.js after setting INFRAI_API_BASE_URL, INFRAI_API_KEY, and INFRAI_EMAIL_PAYLOAD. Generate that payload from the public discovery schema for the email send capability; treating it as JSON input keeps this article from freezing a request shape that the machine-readable contract already defines. Set the base URL to the documented API /v1 base.

type WarmupDay = {
  date: string;
  allowed: number;
  attempted: number;
  paused: boolean;
};

type SendRecord = {
  signupId: string;
  response: unknown;
};

function retryDelayMs(response: Response, attempt: number): number {
  const retryAfter = response.headers.get("retry-after");
  if (retryAfter && /^\d+$/.test(retryAfter)) {
    return Number(retryAfter) * 1_000;
  }
  return Math.min(1_000 * 2 ** attempt, 30_000);
}

async function sendWelcome(
  payload: unknown,
  idempotencyKey: string,
  attempt = 0,
): Promise<unknown> {
  const apiKey = process.env.INFRAI_API_KEY;
  if (!apiKey) throw new Error("INFRAI_API_KEY is required");
  const apiBaseUrl = process.env.INFRAI_API_BASE_URL;
  if (!apiBaseUrl) throw new Error("INFRAI_API_BASE_URL is required");

  const response = await fetch(`${apiBaseUrl}/email/send`, {
    method: "POST",
    headers: {
      authorization: `Bearer ${apiKey}`,
      "content-type": "application/json",
      "idempotency-key": idempotencyKey,
    },
    body: JSON.stringify(payload),
  });

  if (response.status === 429 && attempt < 5) {
    await new Promise((resolve) =>
      setTimeout(resolve, retryDelayMs(response, attempt)),
    );
    return sendWelcome(payload, idempotencyKey, attempt + 1);
  }

  if (!response.ok) {
    throw new Error(`Email request failed: ${response.status} ${await response.text()}`);
  }
  return response.json();
}

async function run(): Promise<void> {
  const rawPayload = process.env.INFRAI_EMAIL_PAYLOAD;
  if (!rawPayload) throw new Error("INFRAI_EMAIL_PAYLOAD is required");

  const day: WarmupDay = {
    date: new Date().toISOString().slice(0, 10),
    allowed: 20,
    attempted: 0,
    paused: false,
  };
  const signupId = "signup_1042";
  const records: SendRecord[] = [];

  if (!day.paused && day.attempted < day.allowed) {
    const response = await sendWelcome(JSON.parse(rawPayload), signupId);
    day.attempted += 1;
    records.push({ signupId, response });
  }

  console.log(JSON.stringify({ day, records }, null, 2));
}

void run();
Enter fullscreen mode Exit fullscreen mode

The call uses explicit POST /v1/email/send, reads the bearer key from the environment, surfaces non-success bodies, and treats a 429 as a delayed retry. The stable signup ID is the idempotency key, so repeating the transport attempt cannot create a second logical welcome send within the platform's 24-hour default deduplication window.

The distinction here is easy to miss — transport retries and warmup policy are separate state machines. A 429 means “try this same operation later,” while an exhausted daily allowance means “do not attempt a new operation today.” If both conditions collapse into one retry queue, a delayed batch can cross midnight and quietly consume the next day's allowance without a deliberate review. Persist the signup ID, calendar day, attempt state, and returned response in one transaction around the queue handoff. Then count delivery outcomes separately as polling discovers them.

Rollout ownership belongs with the template decision

A provider-owned template keeps subject, HTML, and text stable while volume changes. That removes one variable from a period when you are trying to understand reputation, and it prevents an ad hoc formatting edit from landing in the same deployment that raises the daily allowance. The application should pass the verification URL and other approved values; it should not assemble a fresh email design for every send.

Application-owned markup can still be the right call. Choose it when templates must be reviewed in the same repository as product code, when local preview tests are a release requirement, or when changing providers without moving templates is more important than a smaller operational surface. Revenue-per-hour is the useful lens here: template portability has value only if it saves more future work than it creates every week now.

For a solo product shipping weekly, I would outsource the undifferentiated rendering work and retain the policy that affects deliverability. That means provider template, application ramp table, application counters.

How should Node.js welcome email deliverability monitoring work?

There are no webhook event pushes in this capability, so poll email events and accept a slower feedback loop. A small signup service can run that review on a schedule, update its bounce and complaint records, and leave the next allowance paused until the result is evaluated. This is a capability boundary, not an invitation to fake real-time state.

The catch is material. Infrai is not suitable when a bounce or complaint must trigger an immediate event-driven workflow. A provider with webhooks is the better choice then. There is also no SMTP relay, so keep an established SMTP-based system if rewriting that delivery boundary has no product payoff.

No drama. Ship the link, inspect the evidence, then decide whether to raise the cap.

Know the exit conditions

The runner-up is whichever provider your product already knows how to operate. Stay with Postmark or Resend when focused email tooling and webhook-driven handling are central to the signup path. Stay with SendGrid when templates and event consumers already live there. Stay with Amazon SES when AWS event plumbing, permissions, and mail operations are established capabilities rather than new chores.

Also keep template rendering in the application when copy changes need repository review and local tests. That choice adds work, but it makes ownership explicit. The wrong move is migrating providers during warmup merely to make the architecture diagram look tidy; protect the verification path first, because an undelivered link is a failed signup, not an infrastructure curiosity.

For a new solo SaaS with no incumbent, plain REST plus a provider template is the smaller surface. The application still owns the consequential pieces: gradual volume, outcome history, pause decisions, and the queued signups that exceed today's allowance.

References

Top comments (0)