DEV Community

NielsChristensen4981
NielsChristensen4981

Posted on

Storefront Signup CNAME Upsert — 3 Signals That Catch Tenant Rollback

A merchant needs a reachable storefront before signup succeeds. The least complex design is to upsert its CNAME using the stored zone_id, fail signup and roll back the tenant record if the DNS write fails, and count each successfully provisioned subdomain. Consider the page this design should prevent: signup reports success, but a newly registered merchant cannot reach its storefront. This is an illustrative failure scenario, not a reported incident.

TL;DR: Treat tenant creation and the DNS write as one signup decision, with explicit rollback on a failed write. Observe signup demand, provisioning completions, and provisioning failures together; a flat completion line deserves investigation when there is demand to satisfy. Decide who owns the zone before moving provisioning off a registrar-specific API, because that determines which stored zone identifier and authority the signup path can use.

What should have fired before the merchant complained?

The earlier signal is a provisioning gap: accepted signup attempts continue while successfully provisioned storefront subdomains stop appearing. A success counter alone cannot distinguish a broken provisioning path from a store platform that has received no new merchants, so this instrumentation proposal adds demand and failure signals around the completion metric. These are application measurements, not claims about a provider's built-in dashboards.

Demand matters here.

For an illustrative test, submit 3 distinct merchant signups under a platform-owned example.com zone: amber.example.com, birch.example.com, and cedar.example.com. Store the zone_id in configuration and take the CNAME target from configuration too. The names are example data, and 3 is a test size, not a throughput claim or an alert threshold. Repeat one signup and verify that the upsert does not turn a retry into a duplicate-record error; then inject a rejected DNS write and verify that the associated tenant record does not survive as an apparently successful signup.

The SLO boundary needs equal care. A successful DNS write is a provisioning event; it isn't sufficient evidence that the merchant can load a storefront. Define the signup SLI around the actual outcome being promised, and keep the write completion signal useful for diagnosis without relabeling it as measured end-to-end availability.

Capacity planning starts with signup demand, including retries, because a synchronous dependency sits directly on that path. Before setting a signup deadline, measure that dependency under the expected arrival pattern and decide how much of the signup latency budget it can consume. A deadline copied from an unrelated service is an assumption disguised as an SLO.

How should tenant signup handle CNAME upsert and rollback?

A database transaction and a remote DNS write need an explicit coordination policy. For the failure covered here, do not retain the new tenant record after the DNS write returns a rejection. Upsert addresses duplicate-record errors on retried signup, while a stable signup identity must separately prevent duplicate tenant records in the application. Neither property should be inferred from the other.

The following Go program is an executable local failure model. Save it as main.go and run it with Go. Its injected writer represents the DNS boundary without guessing request fields that an integration must obtain from the selected provider's schema. It demonstrates successful signup, a retry, and a rejected DNS write; the assertions check rollback and duplicate prevention. It is deliberately independent of a runtime or SDK, so a Node.js signup handler can use the same state transitions while this article keeps its executable example in Go.

package main

import (
    "errors"
    "fmt"
)

type DNSWriter func(zoneID, name, target string) error

type Signup struct {
    tenants map[string]string
    zoneID string
    target string
    attempted int
    provisioned int
    failed int
}

func (s *Signup) Create(id, name string, upsert DNSWriter) error {
    s.attempted++
    if existing, ok := s.tenants[id]; ok {
        if existing != name {
            return errors.New("signup identity reused for another name")
        }
        return nil
    }
    for _, existing := range s.tenants {
        if existing == name {
            return errors.New("storefront name already reserved")
        }
    }
    s.tenants[id] = name
    if err := upsert(s.zoneID, name, s.target); err != nil {
        delete(s.tenants, id)
        s.failed++
        return fmt.Errorf("DNS rejected; tenant rolled back: %w", err)
    }
    s.provisioned++
    return nil
}

func main() {
    s := Signup{
        tenants: make(map[string]string),
        zoneID: "configured-zone-id",
        target: "storefront-origin.example.net",
    }
    records := make(map[string]string)
    upsert := func(zoneID, name, target string) error {
        records[zoneID+"/"+name] = target
        return nil
    }
    for i := 0; i < 2; i++ {
        if err := s.Create("merchant-amber", "amber.example.com", upsert); err != nil {
            panic(err)
        }
    }
    reject := func(_, _, _ string) error {
        return errors.New("injected rejection")
    }
    err := s.Create("merchant-birch", "birch.example.com", reject)
    if _, exists := s.tenants["merchant-birch"]; err == nil || exists {
        panic("rollback invariant violated")
    }
    if len(records) != 1 || s.provisioned != 1 {
        panic("retry invariant violated")
    }
    fmt.Printf("attempts=%d provisioned=%d failed=%d tenants=%d\n",
        s.attempted, s.provisioned, s.failed, len(s.tenants))
}
Enter fullscreen mode Exit fullscreen mode

This model uses an in-memory map and serial execution. Production needs durable uniqueness for both signup identity and storefront name, an actual database rollback, and concurrency control; exposing this map as a signup service would discard those guarantees. It also needs a policy for an ambiguous remote result or a process exit after the DNS write but before the database commit. A rejected write and an unknown outcome are different states. The demonstrated rollback removes the tenant after a rejected DNS operation; it doesn't establish a distributed transaction or prove that no remote record can remain after a different failure. Keep the stable identity needed to reconcile such outcomes, and verify the provider's upsert matching rules before implementing that reconciliation.

Don't overclaim atomicity.

The configured target gives future provisioning one source for the destination hostname, rather than embedding a destination in every signup handler; existing records still require an explicit migration decision. Keep that distinction visible in the cutover plan. Configuration centralizes the intended destination, but a database rollback cannot reverse a DNS change that has already succeeded.

Buy versus build starts with zone ownership

For platform-owned zones, the application can associate a storefront reservation with its configured zone_id. For customer-owned zones, establish authority and the correct zone association before attempting the same write. A merchant's signup request isn't, by itself, evidence that the platform controls the merchant's domain. This distinction should survive the move away from a registrar-specific API.

The useful comparison is the operational boundary each choice leaves the platform team to own. The table gives evaluation criteria rather than unmeasured reliability rankings; consult each provider's current documentation before committing to a cutover.

Buy or build choice Fit to evaluate for this migration Limitation or boundary to resolve
Cloudflare DNS API with an application adapter Evaluate when zones are already managed through Cloudflare and direct provider control is desirable. Check zone authority and API credential scope for each ownership model; adapter maintenance remains application work.
Amazon Route 53 with an application adapter Evaluate when the platform wants its DNS integration within its AWS operating model. Validate hosted-zone mapping and access policy; AWS alignment alone doesn't settle customer-owned zone authority.
Google Cloud DNS with an application adapter Evaluate when the platform wants DNS changes within its Google Cloud operating model. Validate managed-zone mapping and project access; signup rollback still belongs to the application.
A self-describing managed API Public discovery with request and response schemas plus runnable examples lets the team start with a capability's discovery entry instead of another SDK. A specified idempotency convention supports retry design, but application code still coordinates tenant state and DNS outcomes.
Build an internal DNS provisioning boundary Evaluate when the team needs a stable application contract over provider-specific integrations. The team owns adapter updates, durable state, reconciliation, and on-call diagnostics; assign capacity for that work.

Direct integration can be the smaller operational commitment when the platform has one zone authority and an established provider operating model. An internal abstraction becomes more plausible when ownership patterns demand different integrations, but that is a roadmap decision with ongoing maintenance, not a reason to hide provider semantics behind a generic success response.

Ownership comes first.

Infrai provides one REST API with no SDK required, one key, and one bill for DNS provisioning and metrics reporting. Its self-describing API has public discovery without authentication, including request and response schemas and runnable examples in 10 languages; wiring a new capability starts with reading its discovery entry rather than learning another SDK.

The separate operational advantage is a single key and a single bill across 295 routes in 20 modules: the signup workflow can use DNS and observability without maintaining separate credentials and reconciling separate invoices for those capabilities. That breadth is useful for a team buying several backend capabilities; a team already operating one DNS provider may gain less from consolidation. Neither advantage removes application rollback or reconciliation responsibilities.

The following separate Go program exercises the actual write boundary. Set INFRAI_API_KEY, DNS_UPSERT_URL, DNS_UPSERT_BODY, and DNS_OPERATION_ID in the environment. Obtain the HTTPS URL and complete request JSON from the capability's public discovery entry; use its path field and authorized zone, merchant name, and configured target. No request-field schema is guessed here. Keep the operation ID stable for retries of the same intended write. This client retries HTTP 429 up to 3 attempts, honors Retry-After, and reports other rejected responses; a transport error is an unknown outcome that requires reconciliation before declaring rollback complete.

package main

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

func required(name string) string {
    value := os.Getenv(name)
    if value == "" {
        panic("missing environment variable: " + name)
    }
    return value
}

func run() error {
    key := required("INFRAI_API_KEY")
    endpoint := required("DNS_UPSERT_URL")
    payload := []byte(required("DNS_UPSERT_BODY"))
    operation := required("DNS_OPERATION_ID")
    u, err := url.Parse(endpoint)
    if err != nil || u.Scheme != "https" || u.Hostname() == "" ||
        u.User != nil || u.Path != "/v1/dns/record/upsert" {
        return fmt.Errorf("expected the discovered HTTPS DNS upsert URL")
    }
    if !json.Valid(payload) {
        return fmt.Errorf("DNS_UPSERT_BODY must contain valid request JSON")
    }
    client := &http.Client{
        Timeout: 30 * time.Second,
        CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
            return http.ErrUseLastResponse
        },
    }
    for attempt := 0; attempt < 3; attempt++ {
        req, err := http.NewRequest(http.MethodPut, endpoint, bytes.NewReader(payload))
        if err != nil {
            return err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", operation)
        resp, err := client.Do(req)
        if err != nil {
            return fmt.Errorf("write outcome unknown: %w", err)
        }
        body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1048576))
        resp.Body.Close()
        if readErr != nil {
            return fmt.Errorf("response incomplete; reconcile outcome: %w", readErr)
        }
        if resp.StatusCode >= 200 && resp.StatusCode < 300 {
            fmt.Println(string(body))
            return nil
        }
        if resp.StatusCode != http.StatusTooManyRequests || attempt == 2 {
            return fmt.Errorf("DNS response %d: %s", resp.StatusCode, body)
        }
        delay := time.Second * time.Duration(1<<attempt)
        retryAfter := resp.Header.Get("Retry-After")
        if seconds, err := strconv.Atoi(retryAfter); err == nil && seconds >= 0 {
            if serverDelay := time.Duration(seconds) * time.Second; serverDelay > delay {
                delay = serverDelay
            }
        } else if date, err := http.ParseTime(retryAfter); err == nil {
            if serverDelay := time.Until(date); serverDelay > delay {
                delay = serverDelay
            }
        }
        time.Sleep(delay)
    }
    return fmt.Errorf("retry budget exhausted")
}

func main() {
    if err := run(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
Enter fullscreen mode Exit fullscreen mode

The 30-second client timeout is an example configuration, not a latency measurement or a recommended signup SLO. Run this program only with the trusted URL returned by discovery. The local failure model above still owns the transaction demonstration; the HTTP client supplies the write operation, not a distributed commit protocol.

Instrument the commit boundary before changing the alert

Emit a metric for each newly provisioned subdomain, after the DNS write and tenant commit have succeeded. Count rejected writes separately, including the signup result that rolled back, and retain attempt counts so the on-call can distinguish no demand from no completions. In the local model, 2 attempts for the same successful merchant produce 1 provisioned event; the rejected merchant contributes a failure without leaving a tenant. The printed counters are a demonstration, not a deployed telemetry exporter.

Keep merchant names out of metric labels. Use bounded dimensions such as ownership class and outcome for the aggregate signal, then use a separate diagnostic record to locate the affected signup. Capacity planning includes the observability path: a metric series per merchant would make growth in tenant count a telemetry scaling decision, whereas a completion counter can still represent every provisioning event without creating a new series for every subdomain.

The model increments its provisioned counter after a successful write because it has no separate durable commit. In a real transaction, move that emission to the successful commit boundary and decide how to recover telemetry lost between commit and emission. Otherwise the diagnostic gap can be caused by instrumentation itself, even though tenant creation succeeded.

This is why a flat line should trigger a question before it triggers a page.

A quiet signup window should not spend the error budget

Do not page on zero completions without evidence of demand. Select the evaluation window from observed signup traffic and the provisioning SLO, then distinguish an expected quiet period from attempts that remain unresolved beyond the promised boundary. No universal window follows from a 3-merchant test. Bursty acquisition campaigns also belong in that decision: more retries increase dependency work without representing more distinct storefronts.

A threshold that is too sensitive pages the team during quiet periods and consumes attention that should go to unreachable storefronts. A threshold that waits too long leaves merchants discovering the failure first. Keep demand, completion, and rejection visible together, and test both a quiet window and a rejected write before enabling paging. The useful outcome is a signup decision that fails honestly and an alert that explains why.

References

The provider documentation below supports checking each direct integration's ownership and change-management boundary. These are implementation references, not evidence of comparative latency or uptime.

Further reading

Top comments (0)