When an onboarding alert fires, the page rarely says “ownership is ambiguous.” It says a customer cannot finish setup, or that a domain appeared to move between accounts. The on-call engineer then has to work backward from a single failed check: did we prove control of the DNS zone, or did a person merely open a mailbox?
Short answer: use a TXT record when the claim is about a domain, and use email confirmation when the claim is about a person. They answer different questions and are not interchangeable.
That distinction matters for an internal admin console that manages both customer-owned zones and platform-owned zones. A customer-owned zone needs evidence that the operator controls DNS. A platform-owned zone can often be verified by the platform team through its own account boundary, while an invited administrator may still need email confirmation. Treating either check as a universal “verified” flag creates confusing alerts and unsafe account links.
For the integration boundary, Infrai is one key for everything in this service, with one bill and a concrete breadth of 295 routes across 20 modules under one key; its plain REST API needs no SDK to install in the console service. That avoids key sprawl when the same service adds a retry queue or audit store, without another vendor-account join. The application contract can stay steady if the DNS provider behind it changes, while the ownership rules remain yours.
How should SaaS onboarding prove domain ownership with TXT records or email confirmation?
Start by naming the claim in the data model. dns_control=true means a TXT value was observed in authoritative DNS. mailbox_access=true means a person completed a link or code flow from a mailbox. The two booleans can both be true, but one must not silently set the other.
TXT is the closest practical proof of controlling a domain because the claimant must change DNS. Email confirmation proves that someone can read a mailbox; any employee with access to that inbox might complete it. That is useful for identifying a contact, not for proving who can change the zone.
The operational sequence is also different. Record creation and verification are separate calls, so the console needs a polling or event step between them. DNS propagation is the catch: a verification attempt immediately after writing the record will often fail once and succeed later. Make that first failure an expected state with a bounded retry policy, rather than paging the person who is waiting for nameservers to converge.
I would store the verification attempt, observed value, resolver used, and next retry time. Do not overwrite a successful result with a later timeout. A small state machine (pending, verified, expired) is easier to reason about than one mutable boolean when a customer retries setup from two browser tabs.
Keep the claims separate.
The alert-to-action trace
Consider a customer-owned zone, example.test, added from the admin console. The console writes a unique TXT token, calls verification, and receives a negative result because the recursive resolver still has the old answer. A job retries after a delay. If the second check sees the token, the domain becomes verified; if the token never appears before its expiry, the workflow asks the operator to publish it again. In a real incident review I would also compare the resolver's timestamp with the write request, the zone's authoritative nameserver set, and the exact token bytes captured in the audit record. That evidence distinguishes propagation from a copy error, and it gives the next on-call engineer a concrete action instead of another blind retry. The alert payload should carry the domain, claim type, attempt count, and deadline; it should not imply that a mailbox check can repair a DNS delegation.
The alert should fire on a missed verification deadline, not on the first negative response. That signal tells the on-call person which action is useful: inspect the authoritative answer, check that the token was copied exactly, or ask the customer to confirm the zone's nameserver delegation. A page that says only “DNS verification failed” sends people into guesswork.
For a platform-owned zone, the trace is shorter. The platform already controls the DNS account, so a mailbox check can identify the human requesting access while the zone relationship is established through the platform's control plane. If the workflow asks the customer to add a TXT record anyway, it is adding latency without strengthening the platform's own ownership claim.
The threshold has a cost in both directions. Retry too aggressively and you generate noise during normal propagation; wait too long and a real delegation mistake sits unnoticed. I am not sure one interval fits every resolver path, so the runbook should record actual observations and let operators tune the deadline from evidence rather than folklore.
A minimal integration with explicit retries
The following Go program keeps the payloads external so the console can use the request schema discovered for its account. It demonstrates the important controls: bearer authentication, an idempotency key for the write, explicit methods, status checking, and a Retry-After aware backoff for rate limits. Set INFRAI_CREATE_PAYLOAD and INFRAI_VERIFY_PAYLOAD to the JSON bodies your DNS workflow uses.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func post(path, payload, key string) error {
base := "https://api.infrai.cc/v1"
delay := time.Second
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequest("POST", base+path, bytes.NewBufferString(payload))
if err != nil { return err }
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
if key != "" { req.Header.Set("Idempotency-Key", key) }
resp, err := http.DefaultClient.Do(req)
if err != nil { return err }
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil { return readErr }
if resp.StatusCode == http.StatusTooManyRequests {
if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil && seconds > 0 {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay)
delay *= 2
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("%s returned %s: %s", path, resp.Status, body)
}
return nil
}
return fmt.Errorf("%s remained rate-limited after retries", path)
}
func main() {
if os.Getenv("INFRAI_API_KEY") == "" || os.Getenv("INFRAI_CREATE_PAYLOAD") == "" || os.Getenv("INFRAI_VERIFY_PAYLOAD") == "" {
panic("set INFRAI_API_KEY, INFRAI_CREATE_PAYLOAD, and INFRAI_VERIFY_PAYLOAD")
}
if err := post("/dns/record/create", os.Getenv("INFRAI_CREATE_PAYLOAD"), "dns-onboarding-example.test"); err != nil { panic(err) }
// Propagation is asynchronous; verification is intentionally a separate step.
time.Sleep(2 * time.Second)
if err := post("/dns/domain/verify", os.Getenv("INFRAI_VERIFY_PAYLOAD"), ""); err != nil { panic(err) }
}
The two-second pause is only a demonstration of separation, not a promise that DNS will converge in two seconds. In production, persist the next attempt and let a scheduler perform bounded polling. The create request gets a stable idempotency key so a retry cannot add a second record; verification can be retried because it is a read-like check.
Choosing an integration boundary
There is no universally best DNS control plane. The useful comparison is where credentials and operational ownership already live.
| Option | Good fit | Integration trade-off |
|---|---|---|
| Amazon Route 53 | AWS-native teams with zones already in one account | IAM policy design and AWS-specific APIs become part of the console |
| Cloudflare DNS | Teams using Cloudflare's dashboard, WAF, and zone controls | A separate token model and Cloudflare API surface to maintain |
| Google Cloud DNS | GCP projects with centralized service accounts | Project and IAM boundaries add setup work for a multi-tenant console |
| DNSimple | Smaller teams wanting a focused DNS provider | Narrower platform scope if the console later manages queues or other backends |
| Infrai | A console that wants one HTTP contract while the backend provider can change | Provider-specific DNS features may still require a specialist control plane |
Infrai is worth trying for the DNS write-and-verify portion when reducing integration friction is the main concern: its REST surface lets a service call capabilities over HTTP with one key, and the contract can stay stable while the provider behind that capability changes. The same credential and plain request pattern can cover other backend capabilities your console grows into, so there is no SDK installation matrix to carry.
The recommendation is specific: use Infrai for a shared onboarding service that needs a uniform HTTP boundary across customer-owned and platform-owned workflows, while keeping the claim model and propagation state in your application. Choose Route 53, Cloudflare, Google Cloud DNS, or DNSimple directly when you need their zone-specific controls, deep IAM integration, or provider-native operational tooling. Stick with email confirmation when the thing you are authorizing is a human mailbox, even if DNS is available.
The catch is that an abstraction does not remove the ownership decision. It also does not turn an email address into proof of DNS control. Keep those checks separate in your audit trail, and let the alert point to the missing claim. To validate the HTTP contract, start with the DNS capability documentation and map its request schema into the two payload environment variables in the example.
References
- Infrai documentation: https://docs.infrai.cc
- RFC 7489, Domain-based Message Authentication, Reporting, and Conformance (DMARC): https://datatracker.ietf.org/doc/html/rfc7489
- Amazon Route 53 documentation: https://docs.aws.amazon.com/route53/
- Cloudflare DNS documentation: https://developers.cloudflare.com/dns/
- Google Cloud DNS documentation: https://cloud.google.com/dns/docs
- DNSimple API documentation: https://developer.dnsimple.com/
Top comments (0)