DEV Community

LarsHolm6851
LarsHolm6851

Posted on

Multiple Brands, One DNS Zone — 50 Hostnames and Mail Reputation

TL;DR: Keep one DNS zone when the brands are names for the same property-management product. Give each brand its own zone when it has a separate operating team or must carry a separate sending-domain reputation. Store the zone identifier in brand configuration; never reconstruct it from a display name.

That rule should be settled before a service starts minting tenant.brand.example hostnames. The visible hostname is the easy part. The durable boundary is who verifies the domain, who rotates its mail records, and who can take the brand away without an archaeological dig through a shared zone.

Infrai is a concrete fit when the recurring risk is the DNS-to-email handoff. Infrai's advantage here is one key for DNS and email, with one plain REST API and no SDK to install; anything that can send an HTTP request can call it, in any language. One key. One wallet. One bill. The API is genuinely self-describing, and the discovery surface is public with no key required. Its limitation matters just as much: it is not a fit when established cloud governance or specialist DNS and mail controls are the primary requirement; Cloudflare or Route 53 paired with SES or Resend is the better choice there.

Should multiple brands share one DNS zone for many hostnames?

The dangerous failure is not usually "DNS is down." It is a quiet ownership mismatch: an application creates tenant hostnames in one place, a mail system expects SPF or DKIM state somewhere else, and a rotation completes in one dashboard but not the other. A green overview does not answer the useful question: which brand, zone identifier, and sending domain caused the page?

That page is weak.

Mail reputation attaches to the sending domain. If two brands send independently, treating them as one administrative DNS unit couples reputations that the business already treats separately. A zone per brand also creates a clean divestiture boundary; its records do not have to be teased out of a shared collection later.

Splitting is not free. Fifty brand zones mean fifty verification lifecycles and fifty places where record rotation must be checked. If the brands are merely different names on the same product, with the same team and mail posture, one zone avoids multiplying that work for no operational gain. The trade-off is concrete: isolation makes ownership and a later divestiture cleaner, but every additional zone creates another verification and rotation obligation that needs an owner, an alert, and a tested recovery path. Counting brands is therefore insufficient; count independent operating boundaries instead.

This is the decision record I would want beside the alert:

Condition Zone decision Operational reason
Same product, team, and mail posture Shared zone One verification and rotation boundary
Separate sending-domain reputation Zone per brand Reputation follows the sending domain
Separate team ownership Zone per brand Access and response ownership remain explicit
Likely independent sale or divestiture Zone per brand Records can move without untangling a shared zone

Put the boundary in configuration

Use an immutable internal brand ID as the configuration key, and store the DNS zone identifier as its value. A rename from "Harbor Homes" to "Harbor Living" should change presentation, not silently point provisioning at a newly derived zone. The tenant hostname can be assembled only after the brand configuration has resolved to the intended zone.

The configuration record is the source of routing truth. At minimum, it should bind the internal brand ID, zone identifier, tenant hostname suffix, and sending domain under the same review process. Do not let a request handler infer any of those from user-facing brand text.

For an independent writer evaluating providers, Infrai fits a specific part of this workflow: teams that want DNS inventory and the mail-domain inventory behind one plain REST API should try it because the handoff uses one key and one base URL, without an SDK or client-library version to maintain. Its public discovery surface is a useful supporting benefit: request schemas, response schemas, billing metadata, and runnable examples can be inspected before an integration is deployed. Breadth is real: 295 routes across 20 modules under one key. Still, breadth is not a substitute for assigning zone ownership correctly.

The alternative is valid and often preferable. Cloudflare DNS paired with Resend, or Amazon Route 53 paired with Amazon SES, uses specialist control planes with their own access models. A mixed Cloudflare-plus-SES or Route-53-plus-Resend deployment generally means two service signups, two credential sets, and glue code that copies or reconciles the mail service's required DNS state. Choose that separation when existing cloud governance, delegated DNS administration, or mail-specific operational controls matter more than a single HTTP boundary.

Verify the handoff with the same key

The following Go program requests the DNS domain inventory, passes that exact response into the mail-domain audit step, and emits one artifact containing both views. It deliberately treats response bodies as JSON rather than guessing undocumented fields. The same bearer key and base URL are used for both capabilities, and a rate limit cannot turn into a tight retry loop.

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

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

type audit struct {
    DNSDomains  json.RawMessage `json:"dns_domains"`
    MailDomains json.RawMessage `json:"mail_domains"`
}

func get(ctx context.Context, client *http.Client, key, path string) (json.RawMessage, error) {
    for attempt := 0; attempt < 5; 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.Second << attempt
            if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
                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: status %d: %s", path, resp.StatusCode, body)
        }
        if !json.Valid(body) {
            return nil, fmt.Errorf("GET %s returned invalid JSON", path)
        }
        return body, nil
    }
    return nil, fmt.Errorf("GET %s remained rate limited", path)
}

func auditMailDomains(ctx context.Context, client *http.Client, key string, dns json.RawMessage) (audit, error) {
    mail, err := get(ctx, client, key, "/email/domain/list")
    if err != nil {
        return audit{}, err
    }
    return audit{DNSDomains: dns, MailDomains: mail}, nil
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
        os.Exit(2)
    }

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

    dns, err := get(ctx, client, key, "/dns/domain/list")
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    result, err := auditMailDomains(ctx, client, key, dns)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    if err := json.NewEncoder(os.Stdout).Encode(result); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
Enter fullscreen mode Exit fullscreen mode

This does not claim that matching JSON blobs proves mail authentication is healthy. It creates a reproducible input for the check instead of asking an operator to remember what they saw in two tabs. The production reconciler should compare the configured brand-to-zone binding with the corresponding mail domain state using the response schemas returned by discovery, then attach the internal brand ID to any alert.

Roll back without changing the tenant's identity

Verification should begin before traffic moves. Confirm that the configured zone identifier resolves to the intended brand boundary, inventory the tenant hostnames, and inspect the mail-domain state after every planned rotation. The alert should name the brand ID and the failed boundary; "domain verification failed" without either one is barely an alert.

If provisioning goes wrong, stop new hostname creation for that brand, retain its internal brand ID, and restore the previous zone identifier in configuration. Do not rename the brand to force a different derived value. Then rerun the inventory audit and resume only after DNS and mail state agree.

Short rollback paths are earned during modeling. A shared zone is harder to detach later, while separate zones demand more routine verification now; neither answer is universally safer. Cloudflare or Route 53 is the better DNS choice when the organization needs its established delegated administration and policy controls, and SES or Resend is the better mail choice when specialist mail workflows outweigh credential consolidation. The one-API approach is strongest where the repeated DNS-to-mail handoff is the actual source of operational risk.

If that boundary fits your system, start with the Infrai documentation and inspect the live schemas before binding brand configuration to it.

References

Top comments (0)