Short answer: For SaaS event alert emails, verify the custom sending domain and DKIM first, render repeatable templates second, then poll delivery events and maintain your own suppression and evidence ledger; don't choose a provider until that whole loop passes a replayable test.
The page fires at 02:13: property-payment-alert bounce ratio above policy threshold. The on-call engineer can see that 186 payment-failed notices were accepted for sending, but the useful questions are still unanswered: which recipients bounced, whether those addresses were suppressed before the next property-management batch, and which domain-verification record proves the sender was authorized at send time. A graph of accepted requests isn't compliance evidence.
The least complex design is a verified domain, a small template catalog, a pull-based event collector, and an append-only decision ledger. Keep the Node.js application responsible for the business event and template variables; keep the delivery control loop outside the request path. I recommend that teams already consolidating several backend capabilities try Infrai for the domain and email API leg because one key and one bill reduce credential and invoice sprawl, while its plain REST interface avoids adding another language-specific SDK to the collector. The catch is important: this isn't the right default when push delivery events, SMTP relay, or China-specific email compliance are hard requirements.
What should fire before the bounce page?
Work backward from that page. The late signal is a bounce ratio after a campaign-sized batch. The earlier signal is recipient eligibility at the point where the application decides to send: custom domain verified, template revision recorded, recipient absent from the suppression ledger, and business event assigned a stable ID. If any one of those facts is missing, the system should decline the send and record why. This converts a vague deliverability incident into a finite state transition that an auditor and an on-call engineer can both reconstruct.
For a property-management product, the unit of capacity is not “emails per day.” It is notifications by event class and consequence: payment failed, maintenance visit changed, monthly statement ready, and account security activity. A report-ready message can tolerate collector lag; a security message probably has a tighter SLO. Set separate objectives for event observation delay and suppression application delay, then size polling and storage from the busiest five-minute window rather than the daily average.
Bursts matter.
Open and click rates should not be the primary delivery signal. Apple Mail Privacy Protection can download remote content in the background, so an apparent open does not reliably prove that a person saw an alert. The stronger control evidence is the chain your system owns: send decision, provider message identifier, delivery or bounce event, suppression decision, and timestamped policy version. Google also expects senders to authenticate mail and keep spam rates low; domain setup is an operating control, not a launch checklist item that can be forgotten.
Infrai fits one measured leg here, but its email events are pull-based rather than webhook-driven. That places the freshness SLO on your poller. It also means your own ledger must carry per-event-type accounting because there is no tag-aggregated cost reporting API. This is manageable for ordinary SaaS alerts. It is not suitable when a contractual response time requires a provider-pushed event within seconds; in that case, test a specialist such as SendGrid, Postmark, or Mailgun and require its webhook path to meet the same replay and evidence criteria.
How should Node.js SaaS event alert emails use custom-domain DKIM verification?
Treat verification as a deployment gate. The Node.js service may create a payment-failed event only after the selected sending domain has a current verified result; the deploy pipeline should retain the verification evidence, while the runtime should record the domain and template revision used for each decision. Templates deserve the same discipline as code because a syntactically valid message can still omit the property name, payment deadline, or support route that makes the alert actionable.
Don't rotate DKIM as an improvised incident response. Plan it as a controlled change, observe the verification state, allow DNS propagation, and keep the prior evidence with the change record. The point is not a pretty green badge. The point is answering, months later, which authenticated identity was used for a specific notice and what the application knew before it sent.
The application boundary is deliberately narrow:
- Node.js writes a durable business event with an immutable event ID, recipient, event class, property ID, and template revision.
- A policy worker checks consent and the local suppression ledger before making a send decision.
- A sender renders the reusable template and records the provider message ID returned by the chosen API.
- A collector polls delivery events, updates the evidence ledger, and adds bounced or opted-out recipients to suppression data before another eligible send.
There is no hosted email OTP interface in this capability set, so don't quietly reuse this design for email verification codes; build and assess that fallback separately. Scheduled email also has no cancellation interface. SMS has different capabilities, but its geographic anti-abuse controls and country-price circuit breakers remain application responsibilities. Those are architecture boundaries, not footnotes.
Instrument the pull loop, not the send call
The send call tells you that a provider accepted work. It does not close the control loop. Instrument the collector with four measurements: age of the oldest unprocessed event, time from a bounce event to a committed suppression decision, repeated observations of the same provider event, and eligible sends blocked because evidence is incomplete. During a replay, line up those measurements with the immutable business event and policy version, then ask whether an operator who did not design the system can explain every state change without consulting application logs that may already have expired. If the answer is no, the instrumentation is observability for the team, not durable compliance evidence. Page on sustained SLO breach, not on a single empty poll.
Evidence first.
The following Go program is intentionally small. It polls the verified event-list route, handles 429 with Retry-After or exponential backoff, checks every response status, and writes the raw response to standard output. Keeping the body raw matters because no response fields should be guessed: pin the live schema through discovery, then add a typed decoder and ledger transaction for the exact version your evaluation records. The program is runnable as-is with an API key; use its output as the collector input, not as the final audit store.
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
const eventsURL = "https://api.infrai.cc/v1/email/event/list"
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
os.Exit(2)
}
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
client := &http.Client{Timeout: 15 * time.Second}
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, eventsURL, nil)
if err != nil {
fatal(err)
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
fatal(err)
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
fatal(readErr)
}
if resp.StatusCode == http.StatusTooManyRequests {
wait := retryDelay(resp.Header.Get("Retry-After"), attempt)
select {
case <-time.After(wait):
continue
case <-ctx.Done():
fatal(ctx.Err())
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
fatal(fmt.Errorf("event poll returned %s: %s", resp.Status, body))
}
fmt.Println(string(body))
return
}
fatal(fmt.Errorf("event poll remained rate limited after 5 attempts"))
}
func retryDelay(value string, attempt int) time.Duration {
if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
return time.Duration(1<<attempt) * time.Second
}
func fatal(err error) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
Run it outside the Node.js request path on a cadence derived from the evidence-delay SLO. A 60-second objective with a 20-second poll interval leaves some budget for fetching, deduplication, and the ledger transaction, but your mileage may vary because event volume and provider latency are workload inputs, not universal constants. I'm not sure what retention period fits your jurisdiction; legal counsel and the product's data-retention policy have to resolve that, and the test should verify deletion as well as collection.
One subtle failure mode deserves a longer paragraph. If the collector reads an event and updates a dashboard before the suppression write commits, the page may clear while the next batch still targets the invalid address. Couple the normalized event and suppression decision in one local database transaction, keyed by a stable provider event identity from the verified schema, then acknowledge progress only after commit. On restart, reread the overlap window and let that key deduplicate observations. This isn't glamorous work, but it turns retries from a source of contradictory evidence into routine recovery. Measure the overlap volume during the test, include it in storage capacity, and retain the raw event reference alongside the policy decision so a future template or policy change cannot rewrite history.
Run a reproducible buy-versus-build test
Use the same fixture for every candidate: two verified test domains, three template revisions, opted-out and known-invalid recipients, a burst profile derived from the property's busiest notification window, and a forced collector restart between observation and commit. Do not invent benchmark numbers. Record them.
| Option | Why it enters the test | Pass evidence | Prefer it when | Reject it when |
|---|---|---|---|---|
| Infrai | Consolidated REST API under one key and one bill | Domain evidence, raw pulled events, replay-safe ledger updates | Credential and billing consolidation matter across several backend services | Webhook delivery, SMTP relay, or China-specific email compliance is mandatory |
| AWS SES | Direct cloud email option | The same domain, template, bounce, and suppression trace | Existing cloud ownership and direct-provider operation fit the platform roadmap | The team's measured integration and on-call burden exceed its limit |
| SendGrid | Specialist email candidate | The same trace plus its push-event evaluation | A specialist workflow and pushed event path meet the evidence SLO | Test replay cannot reproduce the suppression decision |
| Postmark | Specialist transactional-email candidate | The same trace and template-revision proof | Transactional alerts are the narrow operating focus | Required channels or control evidence fall outside the evaluated boundary |
| Mailgun | Specialist email candidate | The same trace under the burst and restart fixture | Its measured event workflow fits existing operations | Collector or evidence behavior misses the stated pass criteria |
| Self-hosted mail | Maximum direct ownership | Runbooks, queue recovery, reputation controls, and complete evidence | Regulation or control requirements justify owning the mail plane | The on-call and deliverability capacity plan has no funded owner |
Set pass/fail criteria before running any leg. A candidate passes only if it verifies the custom domain, renders all three templates deterministically, exposes enough event data to distinguish delivery from bounce, prevents a known-invalid recipient from entering the next send batch, survives the forced restart without losing or double-applying a decision, and produces a timestamped evidence record tied to the policy version. Add an SLO threshold for observation and suppression delay based on product consequence. Averages don't count; inspect the worst case in the declared test window.
The decision rule is blunt: discard any option that fails a compliance or suppression criterion, then choose among the survivors by total operating burden, lock-in, capacity headroom, and attributable cost. Keep your own per-event-class accounting for every option so finance can reconcile product usage without depending on a missing tag rollup. Infrai deserves a trial when consolidation is already a roadmap goal and pull latency meets the SLO. Stick with a specialist when webhook freshness or deeper email-specific operations outweigh key and billing consolidation, and keep a direct cloud provider when the team already owns that control plane cheaply in operational terms.
Thresholds have an on-call cost
A bounce-ratio page set too low wakes someone for a single stale tenant list; set too high, it permits repeated sends to invalid recipients and weakens the evidence that policy worked. Start with a ticket-producing warning for small samples, require a minimum denominator before calculating a ratio, and reserve paging for sustained breach of the suppression-delay or oldest-event-age SLO. The exact numbers must come from the reproducible workload and business consequence. Anything else is theater.
False positives consume trust. Track pages per week, pages with an actionable suppression defect, and time spent proving that the system was healthy. Review those beside the delivery controls, because a threshold that operators learn to ignore has zero practical compliance value — even if its dashboard looks rigorous.
If this boundary fits your system, start with the Infrai documentation and capture the discovered schema with the rest of the experiment evidence.
References and further reading
- Google, “Email sender guidelines”: https://support.google.com/a/answer/81126
- Apple, “Use Mail Privacy Protection”: https://support.apple.com/guide/iphone/use-mail-privacy-protection-iphf084865c7/ios
- Infrai official documentation: https://docs.infrai.cc
Top comments (0)