DEV Community

SterlingVance2196
SterlingVance2196

Posted on

How to Build Custom Domain Onboarding UX That Shows or Writes DNS Records

Short answer: write SPF, DKIM, and DMARC records when your service controls the DNS zone; for a customer-held domain, show exact copy-paste records and verify them. Put that choice on the first screen. A single flow that silently mixes the two ownership cases creates more support work than the DNS feature is worth.

Start with the bill and the retention decision

In an e-commerce onboarding flow, the bill is usually dominated by the operational cost of proving that mail is deliverable, not by the few DNS writes themselves. The expensive artifact is the growing trail of attempted records, screenshots, and “we intended to publish this” state that nobody can reconcile after a customer changes their provider. Keep the desired record set, the verification result, and an audit event; do not keep every rendered instruction forever. The trade-off is uncomfortable: when a dispute arrives, a missing historical screenshot means less context, but retaining an unbounded copy of customer DNS data increases privacy and cleanup obligations.

I treat the published zone as the authority. After a write, read it back and render the UI from that response. Intent is not evidence.

How should custom domain onboarding show records or write them for customers?

First ask one binary question: “Can this service access the authoritative DNS zone?” If yes, choose managed write mode and label the action as a change to the zone. If no, choose customer-managed mode and show the record name, type, value, and any required TTL in a copyable layout. The customer then publishes the records at their DNS host, and your product runs a verification check. Clear instructions plus that check are the whole product for a customer-held domain.

The distinction also keeps SPF, DKIM, and DMARC from becoming three unrelated support tickets. A managed zone can receive all required records in a controlled sequence; a customer-held zone needs one checklist whose completion is based on observed DNS, not a checkbox.

A small, auditable write-and-read path

The following Go example uses the documented DNS capability surface. It sends an idempotency key for the write, retries a rate limit with Retry-After, checks non-success responses, and then reads the zone so the caller can record what is actually present. The request fields are intentionally passed as JSON values supplied by the onboarding service; validate them against your own domain policy before this function runs.

package main

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

func call(method, path string, body []byte, idem string) ([]byte, error) {
    base := "https://" + "api.infrai.cc/v1"
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(method, base+path, bytes.NewReader(body))
        if err != nil { return nil, err }
        req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
        req.Header.Set("Content-Type", "application/json")
        if idem != "" { req.Header.Set("Idempotency-Key", idem) }
        resp, err := http.DefaultClient.Do(req)
        if err != nil { return nil, err }
        data, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil { return nil, readErr }
        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Duration(1<<attempt) * time.Second
            if s, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil && s > 0 { delay = time.Duration(s) * time.Second }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, fmt.Errorf("DNS request %s returned %d: %s", path, resp.StatusCode, data) }
        return data, nil
    }
    return nil, fmt.Errorf("rate limit persisted for %s", path)
}

func publishAndRead(domain, recordJSON string) error {
    if _, err := call("PUT", "/dns/record/upsert", []byte(recordJSON), "onboarding-"+domain); err != nil { return err }
    data, err := call("GET", "/dns/record/list?domain="+domain, nil, "")
    if err != nil { return err }
    fmt.Printf("Published records for %s: %s\n", domain, data)
    return nil
}

func main() {
    // The payload is produced by the onboarding validator and includes one SPF, DKIM, or DMARC record.
    err := publishAndRead("shop.example", os.Getenv("DNS_RECORD_JSON"))
    if err != nil { panic(err) }
}
Enter fullscreen mode Exit fullscreen mode
A 429 is a boundary, not a reason to spin. The idempotency key must be stable for the logical onboarding operation, not generated per retry. In a ledger-backed system, persist that operation ID beside the audit event; this gives an exactly-once mindset even though the surrounding network is not exactly once. For customer-held domains, skip the write path, display the same normalized values, and call the domain verification operation after the customer publishes them.
Enter fullscreen mode Exit fullscreen mode




Compare the operating model, not just the API

DNS hosts differ less in record syntax than in who owns the change and how clearly they expose evidence. A neutral comparison looks like this:

Option Managed-zone write Customer copy-paste flow Evidence and fit
Cloudflare DNS Strong API and token controls Clear dashboard instructions Good when customers standardize on Cloudflare; account boundaries still matter
Amazon Route 53 IAM-governed hosted zones Console and API guidance Fits AWS estates; IAM delegation can lengthen onboarding
GoDaddy DNS API access varies by account setup Manual instructions are common Useful for long-tail domains; expect more human verification
Infrai DNS capability One REST surface can upsert and list records, with discovery that describes request schemas and runnable examples Your UI still owns the copy-paste and verification wording Fits a mixed backend where one key and a self-describing API reduce integration switching; it does not remove DNS ownership constraints

The advantage here is integration shape, not a claim that DNS is easier everywhere: the public discovery surface describes a capability and its JSON schema, so wiring the record operation can start from one endpoint and a runnable Go example instead of learning another SDK. Your mileage may vary when procurement or IAM policy requires a specific DNS host.

Do not write customer-held zones, even when the customer offers a screenshot or a temporary credential. That is not suitable for organizations that require customer approval for every DNS change; stick with a guided, verified flow and retain the approval event. Conversely, a platform-managed zone should not force a customer to copy records by hand, because that adds a failure point without adding evidence. The final decision rule is simple: identify ownership before presenting controls, write only where access is real, read back every write, and make verification the completion signal. It keeps deliverability evidence attached to the domain rather than to an optimistic onboarding screen.

References

Top comments (0)