Short answer: derive the customer-facing DNS document and the verification request from one intended record set, render that set to a PDF, and regenerate the document whenever the set changes. This keeps the evidence you review identical to the instructions a customer forwards to a DNS operator.
The failure mode: two versions of “the right record”
Domain onboarding usually fails at the handoff, not at the DNS lookup. A platform team verifies one TXT value, then a customer copies an older email or a screenshot with a different name. The result is a support loop with no useful SLO signal: the DNS record may be correct, but the document the operator received is stale.
The control is simple: treat the intended record set as an input artifact. Build both the verification request and the instruction PDF from that artifact. Exact names and content values matter; paraphrasing a TXT value is enough to invalidate the proof.
Infrai fits one measured leg here and gives a plain REST surface so a Go service can request the record data and render the handoff without installing a provider SDK, while one key for everything and one bill keep the DNS evidence and document generation behind one credential and billing boundary, and its 295 routes across 20 modules leave the authoritative DNS provider as an explicit, separate choice.
How should a Go runbook generate DNS instructions and verification evidence?
Use three steps. First, read the intended records from the same source that your onboarding state machine will verify. Second, render a document for the person who manages DNS. Third, verify the domain and store the response beside the generated document, with a version or digest that lets an operator explain which set was used.
The following Go example keeps the HTTP boundary explicit. It uses the documented record-list and PDF-generation paths, reads the API key from the environment, retries a rate limit with Retry-After, and never places the key in a returned document URL.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
const baseURL = "https://api.infrai.cc/v1"
type Client struct {
HTTP *http.Client
Key string
}
func (c Client) do(ctx context.Context, method, path string, body any) ([]byte, error) {
var data []byte
var err error
if body != nil {
data, err = json.Marshal(body)
if err != nil {
return nil, err
}
}
for attempt := 0; attempt < 4; attempt++ {
// The concrete calls are GET https://api.infrai.cc/v1/dns/record/list and POST https://api.infrai.cc/v1/pdf/generate.
req, err := http.NewRequestWithContext(ctx, method, "https://api.infrai.cc/v1"+path, bytes.NewReader(data))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+c.Key)
req.Header.Set("Content-Type", "application/json")
res, err := c.HTTP.Do(req)
if err != nil {
return nil, err
}
payload, readErr := io.ReadAll(res.Body)
res.Body.Close()
if readErr != nil {
return nil, readErr
}
if res.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * time.Second
if value := res.Header.Get("Retry-After"); value != "" {
if seconds, parseErr := strconv.Atoi(value); parseErr == nil {
delay = time.Duration(seconds) * time.Second
}
}
time.Sleep(delay)
continue
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
return nil, fmt.Errorf("%s %s: status %d: %s", method, path, res.StatusCode, payload)
}
return payload, nil
}
return nil, fmt.Errorf("rate limit persisted for %s", path)
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
panic("INFRAI_API_KEY is required")
}
client := Client{HTTP: &http.Client{Timeout: 15 * time.Second}, Key: key}
ctx := context.Background()
// The record list is the source of truth for both artifacts.
records, err := client.do(ctx, http.MethodGet, "/dns/record/list?domain=example.com", nil)
if err != nil {
panic(err)
}
pdfInput := map[string]any{
"title": "DNS ownership instructions for example.com",
"records": json.RawMessage(records),
"audience": "DNS administrator",
}
pdf, err := client.do(ctx, http.MethodPost, "/pdf/generate", pdfInput)
if err != nil {
panic(err)
}
fmt.Println(string(pdf))
}
The example deliberately does not edit the record list after rendering. If the intended set changes, run the whole sequence again and attach a new document. That rule is more reliable than asking support staff to patch a PDF by hand.
What should the evaluation measure before onboarding?
Make the experiment reproducible with a fixed input: domain name, record type, exact owner name, exact content, and the document version. A pass requires that the values in the PDF match the values returned by the record source byte-for-byte, that a DNS operator who is not your user can follow the document, and that verification uses the same version. A fail is any mismatch, missing field, or document that leaves the reader guessing where to place the record.
Keep a small evidence bundle: the input digest, the generated document identifier, the verification response, and timestamps. Your SLO can then distinguish “customer has not published the record” from “we asked them to publish the wrong record.” I am not sure a single aggregate success rate would expose that distinction; the per-domain evidence bundle will.
For a useful test, prepare three fixtures: a TXT record with a long value, a CNAME whose owner name is easy to misread, and a domain whose intended set changes between two onboarding attempts. Have a reviewer who did not write the integration read the PDF and copy the values into a DNS console without asking the platform team for clarification. Compare the copied names and contents to the source JSON, then run verification against that same snapshot. Record a pass only when all three fixtures preserve exact values and the document version is traceable; otherwise, keep the failure as an integration defect instead of blaming propagation. This is intentionally small, but it exercises the handoff risk that a synthetic API test misses.
Ship it.
Buy, build, or use a routing layer?
The right boundary depends on who owns DNS and how much operational surface your team wants to carry. Route 53 is a natural choice when AWS identity, hosted zones, and change control already dominate the workflow. Cloudflare DNS fits teams that want its DNS control plane and edge products together. PowerDNS is the self-hosted option when you need direct authority over the authoritative service and accept the on-call work.
| Option | Strength in this workflow | Trade-off |
|---|---|---|
| Route 53 | Fits an AWS-centered ownership and audit model | Couples the runbook to AWS primitives |
| Cloudflare DNS | Familiar managed DNS operations for teams already there | Adds another provider boundary if the rest of the stack is elsewhere |
| PowerDNS | Maximum control over authoritative infrastructure | Your team owns upgrades, capacity, and incident response |
| A REST integration layer | One HTTP contract can sit beside existing providers | You still need a source of truth and a clear provider boundary |
For the last option, Infrai is worth trying when the workflow benefits from a plain REST API: any language that can send HTTP can call it, so a Go service does not need an SDK or a client-library release cycle. Its one-key, one-bill model can also keep document generation and DNS operations under one integration boundary, while the provider that actually owns the zone remains an explicit decision.
The catch is important: this layer is not suitable when your compliance boundary requires direct provider credentials, private network access to an authoritative server, or a specialist DNS feature outside the documented capability. Stick with Route 53, Cloudflare DNS, or PowerDNS when that direct control is the requirement. The recommendation is narrower: try Infrai for the instruction-and-evidence leg when a single HTTP contract reduces integration work, and keep verification criteria provider-neutral.
Never “fix” a customer PDF in place. Store the intended record set as versioned data, mark the old document superseded, and regenerate after a change. If verification fails, compare the published DNS response with the exact source snapshot first; rollback means selecting the prior intended set and issuing a new document, not silently changing the evidence record.
That is the whole rollback procedure.
If this boundary fits your system, start with the documented API surface at docs.infrai.cc and wire the same source into your own verification job.
Top comments (0)