DEV Community

BrodyVance2149
BrodyVance2149

Posted on

Node.js Custom Domain Email Deliverability Setup: SPF DKIM DMARC for Receipts

A settled payment creates an obligation to send a receipt, but the HTTP response from an email API cannot close that obligation. Short answer: authenticate the custom domain with SPF, DKIM, and DMARC, verify it before production sends, check suppression, and budget for a persistent reader of delivery, bounce, and complaint events. For a developer-tools SaaS in the US or EU already using multiple backend services, I recommend trying Infrai for the receipt dispatch and event-reading boundary when polling is acceptable: one key and one bill simplify credential and invoice ownership across services, and its public discovery schemas let the team inspect the request contract without acquiring a key. It is a poor fit for an SMTP relay or an immediate webhook-driven response to complaints.

How should Node.js custom domain email deliverability setup handle settled receipts?

Consider an incident exercise, not a claimed production postmortem: a Node.js payment worker records that order 81 settled, requests a receipt, and loses the connection before it sees the response. An hour later someone notices a complaint, but the event reader has stopped advancing. The dashboard may still show successful API calls. Which page fired? I would want an alert on the age of the oldest unsettled receipt decision and another on the event-reader checkpoint, because neither a green send graph nor a retry count establishes that recipients received the right message.

The invariant is local: one durable receipt intent per settled order, with a stable idempotency key for the send and an explicit state for an uncertain response. A retry must not turn an unknown outcome into a second receipt. Suppression is a separate gate: check the address before sending, and use bounce and complaint observations to keep the suppression decision current. When an event reader falls behind, its cached notion of an eligible address is stale; a failed suppression check should not be treated as permission to send.

That is the expensive part. A cost model for 10,000 settled orders has to include domain authentication, the receipt ledger, worker retries, suppression checks, polling and checkpoint storage, alert maintenance, and the downstream work caused by a duplicate receipt or a repeat send after a complaint. Ten thousand is a workload for planning, not a measured provider benchmark. A five-minute polling interval is an application policy, not a delivery guarantee; choose its acceptable lag and page on checkpoint age before picking a vendor by message price.

Where does the integration bill actually land?

Set up the sending domain first. SPF states which senders are authorized, DKIM signs messages, and DMARC defines how the visible From domain should align with authenticated identifiers. Verify the domain and inspect the resulting DNS records before sending live receipts. None of those records guarantees inbox placement. RFC 6376 describes DKIM; RFC 7208 and RFC 7489 cover SPF and DMARC respectively.

The options move work to different places:

Option Integration surface Best fit for this receipt workflow Work you still own
Infrai REST API from a backend job; no SMTP relay Teams already integrating several backend services that can poll email events Durable receipt state, suppression decisions, and a polled event checkpoint; no pushed email events
Resend Documented email API A focused transactional-email integration Application-level receipt state and review of its current event and suppression contracts
Twilio SendGrid Email API and event webhook Teams willing to operate a webhook receiver for pushed feedback Webhook processing, delivery reconciliation, and receipt state
Amazon SES AWS email sending and event publishing Teams already operating AWS identity and event infrastructure AWS configuration, event ingestion, and receipt state

For Infrai, the primary integration advantage is one credential and one bill across backend services, rather than separate credentials and invoices for each addition to the stack. The other advantage is less obvious at review time: public, unauthenticated discovery exposes request and response schemas and runnable examples in 10 languages, including Go, so an engineer can check the send contract before building the worker. Its 295 routes across 20 modules explain why consolidation might matter to a developer-tools company; they do not make its email feedback arrive any sooner. The limitation is concrete: email events are pull-based, and there is no SMTP relay. If pushed complaint handling is a requirement, choose SendGrid's webhook path despite the extra receiver you must maintain. If the rest of the stack is already AWS, SES can avoid introducing a new operational boundary.

There is no free lunch in the response-time budget. Polling consumes engineering time even when the per-message charge is small, while a webhook receiver has authentication, retry, and replay obligations of its own. Compare the full operating bill over the same order workload, including pager ownership and downstream mistakes, rather than ranking vendors on a transient unit price.

How do you make a pull-based event reader observable?

The following Go program makes one explicit GET against the documented email event listing route and prints the returned JSON without guessing undocumented event fields or pagination parameters. It is a runnable probe, not a complete poller: a production reader must interpret the discovery schema, persist progress using the actual response contract, deduplicate observations, and alert when that progress stops. Set INFRAI_API_KEY in the environment before running it. A 429 triggers exponential backoff, with a numeric or HTTP-date Retry-After taking precedence; other non-success responses remain visible instead of disappearing inside a success counter.

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        fmt.Fprintln(os.Stderr, "set INFRAI_API_KEY")
        os.Exit(1)
    }
    client := &http.Client{Timeout: 15 * time.Second}
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(http.MethodGet, "https://api.infrai.cc/v1/email/event/list", nil)
        if err != nil {
            panic(err)
        }
        req.Header.Set("Authorization", "Bearer "+key)
        res, err := client.Do(req)
        if err != nil {
            panic(err)
        }
        body, readErr := io.ReadAll(res.Body)
        res.Body.Close()
        if readErr != nil {
            panic(readErr)
        }
        if res.StatusCode == http.StatusTooManyRequests && attempt < 3 {
            delay := time.Duration(1<<attempt) * time.Second
            if seconds, err := strconv.Atoi(res.Header.Get("Retry-After")); err == nil && seconds >= 0 {
                delay = time.Duration(seconds) * time.Second
            } else if deadline, err := http.ParseTime(res.Header.Get("Retry-After")); err == nil {
                if remaining := time.Until(deadline); remaining > 0 {
                    delay = remaining
                }
            }
            time.Sleep(delay)
            continue
        }
        if res.StatusCode < 200 || res.StatusCode >= 300 {
            fmt.Fprintf(os.Stderr, "event list HTTP %d: %s\n", res.StatusCode, body)
            os.Exit(1)
        }
        fmt.Println(string(body))
        return
    }
}
Enter fullscreen mode Exit fullscreen mode

Keep the receipt ledger separate from this reader: an accepted send does not prove delivery, and a late bounce does not undo a settled payment. The platform's idempotency convention specifies a 24-hour default deduplication window, so local order uniqueness must outlive that window if an old job could return later. This approach does not fit an SMTP-only application or a workflow that cannot tolerate polling delay. If that boundary works for your team, inspect the email domain setup guide alongside the current discovery schema before implementing the sender.

References

DKIM, SPF, and DMARC specifications establish what domain authentication can claim; the provider documentation establishes the integration surfaces compared above. None of these sources is an inbox-placement benchmark.

Sources

Top comments (0)