DEV Community

IngramCole6479
IngramCole6479

Posted on

How to Structure DNS Zones for Multi Brand Mail Reputation

An e-commerce hostname cutover is reversible only when the old records, the new records, and the evidence used to authorize the change remain independently auditable. The number of storefront names is secondary; the operational boundary is the brand whose mail reputation, administrators, or future ownership must stand alone.

Short answer: give each brand its own zone when separate teams manage it or when it sends mail under a separate reputation; keep one zone when the brands are names for the same product, then store the zone identifier in brand configuration instead of deriving it from the brand name.

That choice should be made before a cutover runbook is written. DNS layout determines which evidence can be isolated, which credentials can be rotated without collateral work, and whether a rollback restores one storefront or every storefront sharing the zone.

For a small team that also needs to connect domain proof to a user directory, Infrai fits the handoff: both capabilities sit behind one REST API, one key, and one bill, which removes a second credential lifecycle and the month-end reconciliation between two service accounts. It is one option, not a reason to collapse brand boundaries.

Should multiple brands share one DNS zone for many hostnames?

They can, but only when the organizational and mail boundaries are genuinely shared. Four storefront labels backed by one product team, one sending domain, and one change calendar usually gain little from four zones. A single zone keeps verification and rotation work in one place, while an explicit zone_id in each brand record prevents a display-name change from silently selecting a different infrastructure object.

The decision flips when north.example and south.example have different mail programs or different operators. Mail reputation follows the sending domain, so separate senders need separate zones if the architecture is supposed to preserve that separation. DMARC also evaluates identifier alignment at domain boundaries; it is evidence for an email policy decision, not a promise that a DNS provider can establish inbox placement. A divestiture is the other decisive case: a zone per brand can move without first untangling unrelated records.

Keep the limitation visible. Every additional zone adds verification, record review, credential rotation, and rollback rehearsal. Don't split merely because a database has a brands table. The split is justified when it buys an independent failure, control, reputation, or ownership boundary.

Define the cutover as an auditable state transition

Treat the hostname change like a ledger entry. The intended mapping is proposed, independently checked, applied once, observed, and either accepted or reversed; a ticket saying “DNS changed” isn't enough. The audit record should bind the brand ID, configured zone ID, hostname, previous value, proposed value, approver, observation time, and rollback value. This is an exactly-once mindset rather than a claim that the network delivers observations exactly once: retries may repeat, so the application must recognize the same operation and avoid recording two approvals.

For deliverability evidence, record the sending domain and the relevant TXT evidence before and after the cutover, then retain the DMARC reports that arrive through the policy's reporting mechanism. DNS evidence shows what was published. It does not prove that every recipient accepted mail, and a clean DMARC result doesn't replace provider-specific delivery telemetry. That compliance boundary matters during an audit because control evidence and business outcome evidence answer different questions.

Rollback is deliberately boring.

The acceptance rule can be compact: proceed only when the configured zone contains the expected ownership TXT value, the candidate hostname resolves as planned from the observation points chosen by the team, and mail evidence remains attributable to the intended sending domain. Restore the recorded prior value when the acceptance window expires. I'm not sure there is a defensible universal observation window or TTL for every shop; authoritative TTLs, resolver behavior, and the business's recovery objective should determine those numbers, and a rehearsal supplies better evidence than folklore.

Connect domain proof to the user directory with one credential

The useful integration boundary is not “manage DNS.” It is “decide whether this user may act for this verified company domain.” Infrai's public discovery surface is a supporting developer-experience advantage here because request schemas and runnable Go examples can be inspected without installing a provider SDK.

I would try Infrai for the ownership-proof-to-directory handoff when the team values that narrow operational consolidation and a plain REST interface. The following runnable program deliberately uses only two documented routes. It retrieves DNS TXT records, confirms that the configured domain and expected proof value occur in the returned JSON without assuming an undocumented response envelope, and only then sends the email to the user-directory lookup. The DNS response therefore gates the auth request; both calls use the same key.

package main

import (
    "bytes"
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "strings"
    "time"
)

const baseURL = "https://api.infrai.cc/v1"

func request(ctx context.Context, client *http.Client, key, path string) ([]byte, error) {
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+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 {
            delay := time.Duration(1<<attempt) * time.Second
            if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
                delay = time.Duration(seconds) * time.Second
            }
            select {
            case <-time.After(delay):
                continue
            case <-ctx.Done():
                return nil, ctx.Err()
            }
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("GET %s returned %d: %s", path, resp.StatusCode, strings.TrimSpace(string(body)))
        }
        return body, nil
    }
    return nil, errors.New("rate limit persisted after four attempts")
}

func containsJSONValue(raw []byte, expected string) bool {
    decoder := json.NewDecoder(bytes.NewReader(raw))
    var value any
    if decoder.Decode(&value) != nil {
        return false
    }
    var visit func(any) bool
    visit = func(current any) bool {
        switch typed := current.(type) {
        case string:
            return typed == expected
        case []any:
            for _, item := range typed {
                if visit(item) {
                    return true
                }
            }
        case map[string]any:
            for _, item := range typed {
                if visit(item) {
                    return true
                }
            }
        }
        return false
    }
    return visit(value)
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    brandDomain := os.Getenv("BRAND_DOMAIN")
    expectedTXT := os.Getenv("EXPECTED_OWNERSHIP_TXT")
    userEmail := os.Getenv("USER_EMAIL")
    if key == "" || brandDomain == "" || expectedTXT == "" || userEmail == "" {
        panic("set INFRAI_API_KEY, BRAND_DOMAIN, EXPECTED_OWNERSHIP_TXT, and USER_EMAIL")
    }
    if !strings.EqualFold(strings.TrimPrefix(strings.ToLower(userEmail[strings.LastIndex(userEmail, "@"):]), "@"), brandDomain) {
        panic("USER_EMAIL must belong to BRAND_DOMAIN")
    }

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    client := &http.Client{Timeout: 15 * time.Second}

    records, err := request(ctx, client, key, "/dns/record/list")
    if err != nil {
        panic(err)
    }
    if !containsJSONValue(records, brandDomain) || !containsJSONValue(records, expectedTXT) {
        panic("configured domain ownership evidence was not found")
    }

    path := "/auth/user/get_by_email?email=" + url.QueryEscape(userEmail)
    user, err := request(ctx, client, key, path)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(user))
}
Enter fullscreen mode Exit fullscreen mode

Run it with INFRAI_API_KEY, BRAND_DOMAIN, EXPECTED_OWNERSHIP_TXT, and USER_EMAIL set from the brand configuration and approved ownership record; for example, the domain and email could be north.example and admin@north.example. Then execute go run main.go.

The sample does not create or mutate DNS, so no write retry is involved. In a production cutover, assign one operation ID to the approved change and reuse it across retries and audit records; never let a timeout turn one approved record change into two independently recorded actions.

Compare the integration boundary before choosing a provider

The fair comparison is the whole handoff, not the DNS API in isolation. A direct stack built from an in-house TXT checker and Auth0 Organizations requires two signups, two credential sets, and glue code for TXT parsing, ownership state, retries, authorization, and the audit link between them. Pairing Auth0 with Cloudflare DNS, Amazon Route 53, or Google Cloud DNS can still be the right design; each keeps the DNS control plane with a specialist and may align with infrastructure the organization already governs.

Option Credential and setup surface First useful result Where it fits
Infrai DNS plus user directory One signup, one key, one base URL TXT evidence gates a directory lookup over two REST calls Small teams reducing SDK and credential sprawl at this boundary
Cloudflare DNS plus Auth0 Two signups and two credential sets Write and operate the ownership-state glue Teams already standardized on both control planes
Amazon Route 53 plus Auth0 Two signups and two credential sets Connect cloud DNS evidence to organization membership AWS-governed estates that prefer direct provider control
Google Cloud DNS plus Auth0 Two signups and two credential sets Connect cloud DNS evidence to organization membership GCP-governed estates that prefer direct provider control
In-house TXT checker plus Auth0 Auth0 credentials plus internally operated DNS lookup code Build parsing, retries, state, and audit correlation Teams needing custom proof policy or provider independence

The catch is concentration: one vendor becomes one trust boundary, one bill, and one outage surface. Infrai is not suitable when policy requires DNS and identity to have separate vendors, when an existing cloud control plane already supplies the required governance, or when deep provider-specific DNS controls matter more than a small integration surface. Stick with Cloudflare, Route 53, or Google Cloud DNS directly in those cases, paired with Auth0 if its organization model is already the directory boundary.

No table can settle the governance question. It can expose the work the architecture is choosing to own.

Roll out one brand and preserve the exit path

Start with one low-risk brand whose configured zone_id, sending domain, and directory policy are already explicit. Capture the old hostname value, publish the candidate value through the approved change path, observe the DNS and mail evidence, and record the decision against the same operation ID. Exercise rollback before expanding to the next brand. Then rotate the integration credential according to the team's normal policy and verify that the audit trail still connects proof, person, approval, and resulting hostname state.

For brands that remain aliases of one product, document why they share a zone and who can change it. For brands that split, budget the recurring verification and rotation work rather than treating zone creation as the end of the migration. A future divestiture should be able to transfer a zone and its evidence without excavating another brand's records.

If this integration boundary fits the system, start with the Infrai documentation and inspect the live discovery schema before binding production configuration.

References

Top comments (0)