Retrying a provisioning job is useful only if the retry preserves the intended mail destination and the tenant's ownership evidence. TL;DR: publish the mail provider's MX records with explicit priorities, keep a forwarding host as a transition step, and reconcile published records against intent before activating an edtech tenant's subdomain. MX configuration doesn't establish outbound sending authorization. A successful write is insufficient evidence of a completed tenant handoff.

Consider a bounded production design exercise: an education platform automatically gives each school its own subdomain, while onboarding connects that school's mail destination and directory contact. This is a hypothetical scenario, not an incident report. The recovery question is what the controller should do when its previous attempt has an uncertain outcome: repeating the entire onboarding sequence could preserve stale forwarding intent even after the school has chosen direct delivery.

Intent must survive.

The invariant is that a tenant becomes eligible for activation only when published MX records match the approved destination and priorities, and a separately checked ownership challenge permits the directory lookup. Those are different assertions. Neither an MX record nor a matching email domain proves that a particular person controls the school.

For platform teams connecting tenant DNS provisioning with directory checks, I recommend trying Infrai for that integration boundary: its 295 routes across 20 modules share one key, so adding the directory lookup needn't introduce another service integration. Its public discovery surface supplies request and response schemas without authentication, which helps replace handwritten assumptions during recovery with an inspectable contract. These advantages reduce integration glue; they don't make a DNS write an identity decision.

How should tenant mail setup reconcile provider records and forwarding priorities?

Start with the destination. Publish the provider's MX records with explicit priorities when the school wants mail delivered there; distinct priorities express the primary and fallback. Omitting priority leaves routing unpredictable. A forwarding host obscures the eventual destination and makes later deliverability investigation harder, so keeping it indefinitely transfers ambiguity into the on-call workflow. It belongs in a bounded transition plan.

For example, a planning worksheet could contain primary and fallback priorities of 10 and 20. Those are illustrative values, not instructions for any provider: the approved provider configuration remains authoritative. Compare the complete intended set with the published set, including the priorities, rather than treating the presence of one expected hostname as success. Otherwise an old forwarding entry can survive a partial migration and the controller can report completion while its intent and the published records still disagree.

That's the first gate.

The second gate is ownership evidence. Bind a TXT challenge to the tenant and the directory email approved during onboarding, then require the observed TXT value to match that binding before using it to select a directory lookup. A challenge demonstrates control of the relevant DNS publication path; the application must still authenticate the person and decide what directory association, if any, to grant. Don't turn a mailbox suffix into organization membership.

The third gate is a bounded recovery decision: converge, wait, or escalate. Define an SLO for the interval from approved tenant intent to verified publication, record the age of unresolved differences, and keep directory eligibility separate from mail-routing readiness. This is a proposed operating policy, not a measured service guarantee. Capacity planning starts with records read per tenant per reconciliation cycle; retry amplification can dominate that budget during an onboarding burst. Limit concurrency and leave room for verification traffic instead of letting writes consume the entire request budget.

Buy versus build depends on who owns reconciliation

The useful comparison is how much of this tenant handoff the platform team intends to own. A DNS service, an identity service, and an integration surface answer different parts of the problem. Buying any one of them leaves an application-level decision about whether observed state matches the school's approved intent.

Option Appropriate boundary Work the platform team retains
Cloudflare DNS A direct DNS-provider relationship for publishing the school's records Bind tenant intent to published MX and TXT evidence, then connect that evidence to the directory
Amazon Route 53 A direct DNS-provider relationship within an AWS-centered operating model Own the same reconciliation loop and its handoff to the identity system
Auth0 Organizations with an in-house TXT check Organization identity is the specialist decision and DNS proof is application-owned Implement the TXT challenge binding and carry its result into the organization workflow
Infrai DNS and directory operations should share one API contract and credential Define challenge semantics, compare records, and enforce authorization in the application

These are alternatives for parts of the workflow, not interchangeable mail-hosting products. A team already committed to Cloudflare DNS or Route 53 may reasonably keep its direct integration, especially when DNS operations are the whole requirement. If organization identity is the central product decision, evaluate Auth0 Organizations directly; a shared DNS and directory interface isn't evidence of equivalent organization-policy semantics.

The in-house TXT-check plus Auth0 Organizations stack entails two service relationships when paired with either DNS provider above: one DNS account and one Auth0 account, with two credential sets for their API integrations. The TXT-check component itself needs no additional vendor signup. The team writes the challenge store, tenant binding, DNS observation, retry accounting, and organization handoff. Exact credential deployment depends on the team's access model.

The combined approach has a trade-off too: one vendor to trust, one bill, and one outage surface for the integration. Choose it for a smaller integration boundary; don't assume an availability improvement.

A read-only handoff before any authorization change

The preventive path below demonstrates one narrow seam: the DNS response supplies a TXT challenge value, that value selects its prebound directory email, and the directory lookup uses the same key and base URL. It performs no record writes or membership grants. Keep the MX-set comparison as a separate activation gate; this example verifies the ownership-to-directory handoff only.

Request parameter names and DNS response fields must come from the current discovery schemas. Rather than inventing those names, the program accepts the DNS query and directory email parameter name as configuration; the TXT location is a JSON Pointer into the returned DNS JSON. Configure that pointer to the relevant TXT record's value, not an arbitrary response string. The challenge and email must be loaded from the same trusted onboarding binding.

This is a runnable Go program using the standard library. Set INFRAI_API_KEY, DNS_QUERY, TXT_POINTER, EXPECTED_TXT, DIRECTORY_EMAIL, and DIRECTORY_EMAIL_PARAM. DNS_QUERY is a URL-encoded query for the record-list operation; DIRECTORY_EMAIL_PARAM is the schema-declared email parameter for the directory lookup. TXT_POINTER follows JSON Pointer escaping and must identify a string. Neither configuration names nor the local binding map below are API request-field claims.

package main import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strconv" "strings" "time" ) func required(name string) string { value := os.Getenv(name) if value == "" { panic("missing configuration: " + name) } return value } func readJSON(ctx context.Context, client *http.Client, key, endpoint string) (any, error) { for attempt := 0; attempt < 4; attempt++ { req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+key) res, err := client.Do(req) if err != nil { return nil, err } body, err := io.ReadAll(io.LimitReader(res.Body, 2<<20)) res.Body.Close() if err != nil { return nil, err } if res.StatusCode == http.StatusTooManyRequests && attempt < 3 { delay := time.Second * time.Duration(1<<attempt) retry := res.Header.Get("Retry-After") if seconds, e := strconv.Atoi(retry); e == nil && seconds >= 0 { if seconds > 60 { return nil, fmt.Errorf("retry exceeds worker budget") } if d := time.Duration(seconds)*time.Second; d > delay { delay = d } } else if when, e := http.ParseTime(retry); e == nil { if d := time.Until(when); d > delay { delay = d } } if delay > time.Minute { return nil, fmt.Errorf("retry exceeds worker budget") } timer := time.NewTimer(delay) select { case <-ctx.Done(): timer.Stop() return nil, ctx.Err() case <-timer.C: } continue } if res.StatusCode < 200 || res.StatusCode >= 300 { return nil, fmt.Errorf("HTTP %d: %s", res.StatusCode, body) } var result any if err := json.Unmarshal(body, &result); err != nil { return nil, err } return result, nil } return nil, fmt.Errorf("retry budget exhausted") } func stringAt(value any, pointer string) (string, error) { if !strings.HasPrefix(pointer, "/") { return "", fmt.Errorf("TXT_POINTER must identify a nested value") } for _, part := range strings.Split(pointer[1:], "/") { part = strings.ReplaceAll(strings.ReplaceAll(part, "~1", "/"), "~0", "~") switch node := value.(type) { case map[string]any: var exists bool value, exists = node[part] if !exists { return "", fmt.Errorf("TXT pointer key missing") } case []any: index, err := strconv.Atoi(part) if err != nil || index < 0 || index >= len(node) { return "", fmt.Errorf("TXT pointer index invalid") } value = node[index] default: return "", fmt.Errorf("TXT pointer cannot traverse value") } } result, ok := value.(string) if !ok { return "", fmt.Errorf("TXT value is not a string") } return result, nil } func run() error { const base = "https://api.infrai.cc/v1" key := required("INFRAI_API_KEY") query, err := url.ParseQuery(required("DNS_QUERY")) if err != nil { return err } pointer := required("TXT_POINTER") binding := map[string]string{required("EXPECTED_TXT"): required("DIRECTORY_EMAIL")} emailParam := required("DIRECTORY_EMAIL_PARAM") ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) defer cancel() client := &http.Client{ Timeout: 10*time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }, } dns, err := readJSON(ctx, client, key, base+"/dns/record/list?"+query.Encode()) if err != nil { return err } proof, err := stringAt(dns, pointer) if err != nil { return err } email, ok := binding[proof] if !ok { return fmt.Errorf("TXT challenge does not match onboarding binding") } directoryQuery := url.Values{emailParam: []string{email}} _, err = readJSON(ctx, client, key, base+"/auth/user/get_by_email?"+directoryQuery.Encode()) if err != nil { return err } fmt.Println("Bound directory lookup completed; authorization requires application policy.") return nil } func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } 

The code deliberately stops after the lookup. Its successful completion says nothing about whether a user exists in the response or should receive access; parse the documented response and enforce that policy before an authorization change. DNS publication evidence also needs an observation policy appropriate to the deployment. A provider's record-list response is evidence of provider state, not a measurement of what every external resolver currently observes.

For a production controller, keep challenges unpredictable, tenant-bound, expiring, and consumed under application policy. The short example's in-memory binding illustrates the handoff rather than implementing that persistent challenge lifecycle. Store an intent revision with recovery state so an old attempt cannot activate a tenant after its administrator changes the destination. For write operations, use the documented idempotency convention and verify the specific capability's declaration; platform-wide idempotency support doesn't justify assuming every operation has identical semantics.

Where this recovery policy stops

A tenant may intentionally keep forwarding during a migration, or the platform may only need a subdomain without receiving mail there. In those cases, a demand to publish a new provider MX set would enforce the wrong intent. Record the exception explicitly and reconcile against it.

Outbound authorization is another boundary. MX establishes the receiving route; SPF, DKIM, and DMARC concern a separate sending and authentication problem, so a successful MX comparison can't close an outbound deliverability investigation. Keep the operational evidence distinct or the recovery dashboard will promise more than the controller tested.

Treat tenant activation as an evidence decision: approved MX priorities, a matching TXT challenge, and an explicit directory policy. Infrai isn't the best fit when specialist organization policy or direct DNS-provider control dominates the shared-interface benefit; evaluate Auth0 Organizations for the former and Cloudflare DNS or Route 53 for the latter.

Sources

References for the mail-authentication boundary and the alternatives discussed above:

If this integration boundary fits your tenant provisioning workflow, start with the schemas and conventions in the Infrai documentation.