Consider a proposed page during an e-commerce mail-provider cutover: the sending domain's published records and the mail service's domain status disagree. The on-call sees both observations, their timestamps, and the intended provider. That is a useful page; “domain missing” without lifecycle context is a much weaker one. This is a design example, not a reported incident.

Short answer: read sending-domain status and published DNS records on a schedule, emit both as metrics, and alert when they disagree for an active domain. A daily check fits stable configuration. During an MX cutover, choose a separate observation interval and propagation allowance that match the change window; a daily check cannot support a minutes-long decision.

How should records and mail status expose sending domain health before cutover?

Start with the desired state. The company wants its domain's MX records to point at the selected mail provider, while the sending-domain check also needs the records that provider requires for its domain status. These are related observations, but an MX change alone is not evidence that the sending domain is healthy. Keep the expected record set explicit rather than treating every mail-related DNS record as interchangeable.

The earlier signal is disagreement between that intended configuration, the published records, and the provider's reported status. Either source alone can mislead. A status response without the DNS observation cannot establish agreement; a configured record list without a publication check cannot establish what an external observer sees. Store timestamps beside the observations so an old success does not silently stand in for a fresh check.

Keep both observations.

Freshness is a prerequisite.

For the instrumentation change, emit the DNS comparison result and mail-service status separately, then derive a disagreement signal only when the observations are fresh enough for the same decision window. Keep collection failure distinct from a negative health result. Record the domain's lifecycle state as well: a deliberately retired domain should not page merely because a record or service entry is absent. This is how the monitor can reveal configuration drift, including a record someone edited by hand, without turning normal retirement into an incident.

Treat the SLO as a proposed operational contract: an active domain's disagreement must become visible within the agreed detection window. Neither DNS agreement nor a provider status response measures message delivery, inbox placement, or checkout completion, so those observations cannot substantiate a delivery SLO. The configuration monitor has a narrower job.

A useful cutover review therefore asks for the intended records, the last fresh observation from each side, and the policy that decides when a mismatch becomes actionable. A green dashboard with no observation age is an incomplete answer.

Propagation delay sets the cutover decision window

The decision axis is propagation delay versus cutover speed. A faster scheduler gives the operator more observations; it does not make the underlying transition complete sooner. Establish the propagation allowance before the change, using the actual records and the mail provider's requirements, and keep the previous configuration available to the operator making the cutover decision.

Do not invent a universal wait.

Faster polling has a trade-off: more timely evidence costs more collection work. Choose the daily baseline because stable configuration does not justify continuous cutover-level polling.

For capacity planning, let N be the number of active domains and K the observations per domain per day. The baseline requires N × K domain evaluations per day. As a planning example, 240 active domains checked daily require 240 evaluations; changing all of them to a 5-minute interval means 288 evaluations per domain per day, or 69,120 total. Those are arithmetic examples, not measured traffic or an assertion about any provider's request limits. Each evaluation may require several requests. That difference should influence the roadmap. Apply the faster cadence to domains undergoing an approved change, bound that population, and return them to the daily baseline when the transition is complete. Otherwise a short cutover window quietly becomes permanent polling load.

Thresholds belong to that same capacity discussion. If the collection interval is T and the policy requires a disagreement to persist for W, budget detection time around both T and W, plus collection time; do not promise detection in W alone. Avoid assigning a numeric propagation guarantee without evidence. Select the window against the actual cutover requirement, and make the observation age visible when an operator decides whether to proceed.

A Go ownership gate for a Node.js monitoring workflow

There is a second seam worth protecting during this change: who may associate the company domain with a directory user? Domain ownership proof and the user directory can share an integration boundary, but domain control does not by itself establish a person's employment or authorize every account operation.

The following executable is a narrow ownership gate that a Node.js monitoring workflow can launch. All implementation code is Go. It checks that an application-issued TXT challenge appears in the DNS record-list response, verifies that the same value is published through the configured resolver, and uses the user identifier carried by that verified value to fetch the directory user. Its JSON output can accompany the scheduled health observations; it does not replace the mail-status check.

The challenge format is an application convention, not a provider response schema: company-proof=<nonce>;directory-user=<id>. Before running the program, set DOMAIN, TXT_NAME, PROOF_NONCE, DIRECTORY_USER_ID, API_BASE_URL, and INFRAI_API_KEY, and provision that exact TXT value at TXT_NAME. Set API_BASE_URL to the service's documented HTTPS API base ending in /v1. Use a fresh, unpredictable nonce bound to the authenticated enrollment request. The expected user identifier must come from that request's trusted server-side state. Do not accept an arbitrary identifier supplied by a caller.

Save this as main.go and run it with the Go toolchain. Both requests use the same key and base URL. The first response supplies the candidate value; after exact matching and publication verification, its embedded identifier supplies the second request's path parameter. No DNS response field names are assumed.

package main import ( "context" "encoding/json" "fmt" "io" "net" "net/http" "net/url" "os" "strconv" "strings" "time" ) func required(name string) string { value := os.Getenv(name) if value == "" { panic("missing environment variable: " + name) } return value } func candidate(value any, expected string) string { switch v := value.(type) { case string: if v == expected { return v } case []any: for _, item := range v { if found := candidate(item, expected); found != "" { return found } } case map[string]any: for _, item := range v { if found := candidate(item, expected); found != "" { return found } } } return "" } func getJSON(ctx context.Context, client *http.Client, base, key, path string) (any, error) { for attempt := 0; attempt < 4; attempt++ { req, err := http.NewRequestWithContext(ctx, http.MethodGet, base+path, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+key) resp, err := client.Do(req) if err != nil { return nil, err } body, readErr := io.ReadAll(resp.Body) resp.Body.Close() if readErr != nil { return nil, readErr } if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 { delay := time.Second * time.Duration(1<<attempt) retry := resp.Header.Get("Retry-After") if seconds, err := strconv.Atoi(retry); err == nil && seconds >= 0 { delay = time.Duration(seconds) * time.Second } else if when, err := http.ParseTime(retry); err == nil { delay = time.Until(when) if delay < 0 { delay = 0 } } timer := time.NewTimer(delay) select { case <-ctx.Done(): timer.Stop() return nil, ctx.Err() case <-timer.C: } continue } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body) } var value any if err := json.Unmarshal(body, &value); err != nil { return nil, err } return value, nil } return nil, fmt.Errorf("retry budget exhausted") } func run() error { domain := strings.TrimSuffix(strings.ToLower(required("DOMAIN")), ".") txtName := strings.TrimSuffix(strings.ToLower(required("TXT_NAME")), ".") if txtName != domain && !strings.HasSuffix(txtName, "."+domain) { return fmt.Errorf("TXT_NAME must belong to DOMAIN") } userID := required("DIRECTORY_USER_ID") expected := "company-proof=" + required("PROOF_NONCE") + ";directory-user=" + userID key := required("INFRAI_API_KEY") base := strings.TrimRight(required("API_BASE_URL"), "/") parsed, err := url.Parse(base) if err != nil || parsed.Scheme != "https" || parsed.Host == "" || parsed.Path != "/v1" { return fmt.Errorf("API_BASE_URL must be an HTTPS API base ending in /v1") } ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() client := &http.Client{ Timeout: 15 * time.Second, CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }, } records, err := getJSON(ctx, client, base, key, "/dns/record/list") if err != nil { return err } proof := candidate(records, expected) if proof == "" { return fmt.Errorf("expected challenge absent from record list") } published, err := net.DefaultResolver.LookupTXT(ctx, txtName) if err != nil { return err } matched := false for _, value := range published { if value == proof { matched = true break } } if !matched { return fmt.Errorf("challenge not observed at requested TXT name") } _, verifiedUserID, ok := strings.Cut(proof, ";directory-user=") if !ok || verifiedUserID != userID { return fmt.Errorf("challenge user binding mismatch") } _, err = getJSON(ctx, client, base, key, "/auth/user/get/"+url.PathEscape(verifiedUserID)) if err != nil { return err } return json.NewEncoder(os.Stdout).Encode(map[string]any{ "domain": domain, "ownership_proof_observed": true, "directory_lookup_completed": true, "checked_at": time.Now().UTC().Format(time.RFC3339), }) } func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } 

This gate deliberately grants no permissions. A successful directory lookup still needs application-specific interpretation and an authorization decision; checking a TXT token and receiving a successful HTTP response cannot supply that policy. The resolver observation is also one observation point, not proof that every resolver has converged. Keep that limitation visible during a cutover.

The program performs reads, so it needs no write-idempotency mechanism. It sets a 60 seconds overall timeout and a 15 seconds per-request timeout, retries rate limiting with exponential backoff unless Retry-After specifies a delay, and stops within its context deadline. Schedule it externally at the chosen cadence, preserve failures as collection failures, and report the DNS and mail-status observations through the monitoring integration separately. The ownership gate is an enrollment control; repeatedly passing it is not the definition of sending-domain health.

Buy versus build depends on who owns the seam

Infrai exposes DNS and directory lookups through one REST API with no SDK required, using one key and one bill across 295 routes and 20 modules. The simple API and broad backend coverage let the Go program use its standard HTTP client for both sides of the handoff. Public discovery supplies request and response schemas, which helps the team check the integration contract before wiring it into the cutover workflow. The combined approach means one vendor to trust, one bill, and one shared outage surface.

That consolidation is useful when the platform team owns both sides of the workflow. Existing identity or DNS ownership can make a separate stack the better fit. Compare the work retained by the team, rather than pretending that a single credential removes reconciliation logic.

Buy versus build option Where it fits Work and boundary to retain
Cloudflare DNS with Auth0 Organizations A team that has selected these products for DNS and organizational identity Build the challenge binding, compare published records with mail status, and connect domain proof to the directory policy. Existing adoption can outweigh a new integration boundary.
Amazon Route 53 with Auth0 Organizations A team whose domain operations already belong with its AWS operations Keep the DNS and identity responsibilities explicit. The platform team still owns observation freshness, the cutover decision, and the mapping from a proof to a user.
In-house TXT verification with Auth0 Organizations A team willing to maintain its own proof lifecycle Public TXT lookup requires no API credential, but challenge issuance, expiry, replay handling, and authorization policy remain application work.
Combined API with an application-owned reconciliation policy A team seeking shared access to DNS and directory capabilities Buy the shared service interface; build the desired-state model, mail-status comparison, and alert lifecycle. Consolidation changes the integration boundary, not the definition of health.

For a greenfield in-house TXT check plus Auth0 Organizations, count one identity-service signup and its credentials; public resolver reads add neither. If the application must also manage records through a separately selected managed DNS service, budget a second service enrollment and a second credential set. These are explicit stack assumptions, not a claim that every deployment needs exactly two accounts. The glue is still yours: issue and bind the challenge, observe publication, associate the approved directory user, and retain an auditable authorization decision.

The final threshold deserves more scrutiny than the vendor choice. Page on every absent domain and retired domains create noise. Page on every transient disagreement during an approved transition and propagation becomes an on-call tax. Wait too long and the monitor misses the useful intervention window. Keep the fast schedule scoped to the cutover, show observation ages, and choose the persistence threshold against the detection SLO. A false positive consumes operator attention precisely when that attention is needed to judge the change.

References

RFC 7489 provides the DMARC reference for the authentication-policy part of the discussion; it does not make MX correctness a substitute for sending-domain health. The product documentation below identifies the separate DNS and organizational-identity options considered in the buy-versus-build table.

Further reading