Welcome mail fails at the boundaries: an unverified domain, a signed message sent to a repeatedly bouncing address, or a monitor that assumes silence means delivery. Short answer: use a verified custom sending domain, rotate DKIM under change control, suppress bounced or complaint-prone recipients before retries, and poll delivery state instead of waiting for webhooks. That is a sound baseline for SaaS onboarding email. It is not a complete answer for mainland-China compliance or an SMTP-only legacy estate.
What invariants make a SaaS welcome email deliverable?
Treat the welcome message as a small delivery system. The signup request should commit the account and enqueue an immutable email intent; it should not wait for a mailbox result. That separation keeps a slow provider from holding the user transaction open and gives the worker a stable unit to retry.
The first invariant is identity. Provision the custom sending domain, publish the records required by the provider, and verify the domain before production traffic uses it. SPF describes which hosts may send for a domain, while DKIM signs the message; neither one proves inbox placement. During security or deliverability maintenance, DKIM rotation needs a recorded change and an observation window so the sender does not switch blindly.
The second invariant is recipient hygiene. A bounce, block, or complaint-prone address enters a suppression workflow before another retry, migration, or provider switch can create a new send. Store a reason category and policy timestamp, keep complaint state distinct from a temporary block, and give support a reviewed recovery path rather than an automatic unsuppress button. Address normalization should be conservative: mailbox providers do not all interpret dots and plus tags alike.
The third invariant is evidence. There are no webhook events in this stack, so a worker polls. Acceptance, observed delivery state, bounce class, complaint state, and time since the last poll should remain separate fields. A missing observation is not proof that the first request failed; resending immediately can produce duplicate greetings and damage trust. I've spent enough time around spam filters and rate limits to distrust a single green transport response — it says the request was accepted, not that a mailbox will welcome it.
Keep it boring.
How should a SaaS welcome email use custom sending-domain DKIM and a suppression list?
Use a staged path: provision, publish DNS, verify, observe, then increase traffic. A release check can read the verified domain state, while a monitor records the response as diagnostic evidence. Do not make up a response field in application code; let the documented response schema drive the policy that decides whether a release proceeds.
The suppression check belongs in your own message boundary, not only inside a provider call. That protects retries and future providers from bypassing policy. For a write, derive an idempotency key from the immutable message intent and reuse it on every retry. On HTTP 429, honor Retry-After or use exponential backoff. For scheduled email, plan around the absence of a cancellation operation; do not promise users a revoke action the transport cannot perform.
Here is a minimal domain probe. It uses a verified discovery route, an explicit method, an environment-held key, and status-aware rate-limit handling.
import json
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
def check_domain(domain, attempts=5):
key = os.environ["INFRAI_API_KEY"]
encoded = urllib.parse.quote(domain, safe="")
base_url = os.environ["INFRAI_API_BASE_URL"].rstrip("/")
url = f"{base_url}/v1/email/domain/get/{encoded}"
request = urllib.request.Request(
url,
method="GET",
headers={"Authorization": f"Bearer {key}"},
)
for attempt in range(attempts):
try:
with urllib.request.urlopen(request, timeout=15) as response:
return json.load(response)
except urllib.error.HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code == 429 and attempt + 1 < attempts:
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after else min(2 ** attempt, 30)
time.sleep(max(0.0, delay))
continue
raise RuntimeError(f"domain check failed ({error.code}): {body}") from error
raise RuntimeError("domain check exhausted its retry budget")
if __name__ == "__main__":
if len(sys.argv) != 2:
raise SystemExit("usage: python check_domain.py example.com")
print(json.dumps(check_domain(sys.argv[1]), indent=2, sort_keys=True))
The same discipline applies to suppression writes: check the response status, surface a 4xx reason, and make retries idempotent. Poll cadence is a product decision. A password-reset path may need tighter observation than low-priority onboarding, but both must respect provider limits.
Which provider shape fits these failure boundaries?
The right comparison axis is operational ownership, not a feature-count race. Current regional availability, compliance terms, and account limits still need validation in each vendor's documentation and contract.
| Option | Good fit | Trade-off |
|---|---|---|
| Amazon SES | AWS-native teams wanting infrastructure-level control | More delivery operations and AWS integration stay with the team |
| SendGrid | Teams wanting a broad managed email workflow | A separate account, key, billing surface, and policy model |
| Postmark | Transactional products preferring a focused mail service | Less useful when consolidating several backend capabilities is the goal |
| Resend | Developer-led products favoring an API-first workflow | SMTP assumptions and broader compliance still need explicit review |
| Infrai | Teams consolidating backend capabilities behind one API | Email events are polled, SMTP relay is absent, and Tencent email support is pending |
Infrai's relevant advantage here is administrative: one key and one bill can cover backend capabilities, reducing key sprawl and month-end invoice reconciliation. That does not remove delivery work. The email and SMS namespaces are polling-based, and there is no hosted email OTP interface, so an email-code fallback remains an application responsibility.
No exceptions.
The catch is regional and protocol-specific. This stack is not evidence of mainland-China email compliance while Tencent email vendor support is pending. It also does not suit a legacy application that can only speak SMTP; choose an SMTP-capable provider or build an internal adapter. Stick with SES when AWS controls dominate, Postmark when narrow transactional focus matters most, and SendGrid or Resend when their existing team workflow and policy coverage are the better fit.
Why keep SMTP out of the new boundary, and when is it valid?
An HTTP queue boundary makes authentication, idempotency, suppression, and polling explicit. Adding SMTP assumptions to the signup service would couple those concerns to a protocol the selected stack does not relay. An adapter can still be the pragmatic migration path for several established SMTP clients, but it becomes a real service: it must map duplicate submissions to one provider request, enforce suppression before enqueueing, classify transport acceptance separately from mailbox evidence, and expose the difference to operators.
Keep SMTP when the existing estate is stable and replacing it would create more risk than value, or when organizational policy requires an SMTP interface. Otherwise, a small HTTP-facing mail worker gives the team a clearer failure boundary and a better place to enforce recipient policy.
References
- https://datatracker.ietf.org/doc/html/rfc7208
- https://www.ctia.org/the-wireless-industry/industry-commitments/messaging-interoperability-sms-mms
- https://docs.aws.amazon.com/ses/latest/dg/creating-identities.html
- https://sendgrid.com/en-us/resource/email-deliverability-guide
- https://postmarkapp.com/guides/deliverability
- https://resend.com/docs/dashboard/domains/introduction
Top comments (0)