DEV Community

CianWinslow371
CianWinslow371

Posted on

2026 Node.js DNS Cutover Ledger — Converging Apex A and WWW CNAME State

Short answer: publish the apex A record and the www CNAME from one immutable change record, then make convergence and rollback evidence part of that record. The useful abstraction is a governed ledger entry, not a pair of unrelated DNS buttons.

In a marketplace, a hostname cutover can move a seller storefront, a payment edge, or a regional checkout. The visible request is tiny. The operational object is not. An A record at the zone apex and a CNAME below it have different DNS constraints, separate propagation paths, and often separate provider operations. The failure that matters is drift: the intent says “deployment B,” while one name still publishes deployment A.

The bill is mostly evidence, not DNS writes

DNS write calls are usually the smallest line item in this workflow. The dominant cost is the evidence kept around them: every retry, resolver sample, operator approval, and rollback decision becomes a row, a log event, or an object in durable storage. That is a governance cost even when the DNS provider charges per request.

The number that changes the bill is the sampling policy.

Take a deliberately concrete retention window. A ten-minute verification period sampled every 30 seconds creates 20 observations per name, or 40 observations for an apex/www pair. A fleet of 2,000 seller domains would produce 80,000 observations for one release. If each observation carries a 2 KB resolver payload, that single release leaves roughly 160 MB before indexes, replicas, and log shipping; a five-minute cadence cuts the observation count in half but also gives operators fewer clues about a short-lived split. The arithmetic is simple, yet the design consequence is less obvious: retaining every raw response forever makes audit storage grow with polling frequency rather than with meaningful changes, while sampling too sparsely can make a rollback look instantaneous when customers were still seeing the old target.

I keep the immutable intent, each write attempt, a compact verification result, and the rollback relationship. After the evidence window, raw resolver payloads can be summarized by resolver identity, timestamp, observed value, and a result hash. That reduces retention volume while preserving the answer to “what did we know, and when?”

There is a cost to this deletion. When a regulated seller disputes a stale answer months later, a hash and timestamp cannot explain every DNS flag or transport detail. For those tenants I retain a small raw sample and the authoritative response; for ordinary tenants I document the shorter policy. Retention is a risk decision, not a housekeeping afterthought.

What should a Node.js cutover ledger record before publishing apex A and www CNAME?

The ledger should describe desired state before it describes execution. One row (or event) contains the tenant, zone, both names, target values, TTL policy, an intent version, approver, and a rollback pointer. A second set of records captures attempts and observations without mutating the original intent.

An apex cannot carry a traditional CNAME, while www can. A common pair is therefore an A record at shop.example and a CNAME at www.shop.example; some DNS systems expose an alias-like feature, but the governance contract still needs two explicitly verified names. DMARC is relevant to the surrounding mail policy, yet it does not make these web records atomic; RFC 7489 defines reporting and alignment semantics, not a cutover transaction.

The ledger's state machine should distinguish planned, applying, partial, converged, and rolled_back. A boolean active hides the exact state that an auditor or incident commander needs. The version is monotonic, and the idempotency key is derived from tenant, hostname pair, and version, so a timeout can be retried without creating a second intent.

Keep the business boundary small. The Node.js API can accept one command and enqueue one intent; a worker owns provider adapters and writes an attempt outcome after each record operation. The following Go interface shows the boundary without assuming a commercial SDK:

package dnsledger

import "context"

type Record struct {
    Name  string
    Type  string
    Value string
    TTL   int
}

type Adapter interface {
    Upsert(ctx context.Context, record Record, idempotencyKey string) error
}

func ApplyPair(ctx context.Context, adapter Adapter, tenant string, version int64, apex, www Record) error {
    if apex.Type != "A" || www.Type != "CNAME" || apex.TTL <= 0 || www.TTL <= 0 {
        return ErrInvalidIntent
    }
    base := tenant + ":" + formatVersion(version)
    if err := adapter.Upsert(ctx, apex, base+":apex"); err != nil {
        return err
    }
    return adapter.Upsert(ctx, www, base+":www")
}
Enter fullscreen mode Exit fullscreen mode

The adapter must persist an attempt for each call. If the worker dies after a provider accepted the A update but before the database commit, reconciliation reads the authoritative records and compares them with the immutable intent. It then repeats the same upsert key or records that the desired value is already present. Generating a fresh key on every retry would turn an uncertain response into duplicate history.

The awkward sequence is easy to miss in a diagram. At 09:00:00 the ledger appends version 731. At 09:00:01 the apex write is accepted. At 09:00:02 the process loses its database connection before recording that response. At 09:00:08 the retry submits the apex operation again, then the www operation times out after the provider has accepted it. A dashboard that only counts successful HTTP responses reports zero progress or one success, depending on where it samples. A ledger with per-attempt evidence reports two uncertain operations, and the reconciler can settle them by reading the published values. That distinction matters during a payment release: operators need to know whether to retry, wait, or roll back, and each choice must point to a versioned fact rather than a guess.

How can one Node.js example converge apex A and www CNAME during a rollback?

Treat rollback as a new version, not an edit to history. Suppose version 731 points a seller's storefront to deployment B. Version 732 points both names back to deployment A. The worker applies both records, while the reconciler checks authoritative answers and records the observation against version 732. Version 731 remains visible as the cause of the rollback.

The application can expose a simple status endpoint that reads the ledger, but the status must be evidence-based. “Accepted” means the provider acknowledged a request. “Converged” means both authoritative answers match the same version; recursive observations can then measure customer cache exposure without pretending to be proof of global completion.

type Observation struct {
    Name       string
    Observed   string
    Resolver   string
    AtUnix     int64
    Intent     int64
}

func Converged(intent int64, apex, www Observation, expectedA, expectedCNAME string) bool {
    return apex.Intent == intent && www.Intent == intent &&
        apex.Observed == expectedA && www.Observed == expectedCNAME
}
Enter fullscreen mode Exit fullscreen mode

A 60-second TTL is a cache-control input, not a promise that every recursive resolver refreshes on that schedule. I am not sure which resolvers a buyer's ISP will honor perfectly, so the release gate should state an observation window and a tolerated stale fraction instead of claiming an exact worldwide deadline. During that window, route health checks by deployment identity and include the hostname plus resolved target in application logs; otherwise a mixed-cache report is impossible to interpret.

Where does this pattern stop being the right tool?

The catch is that the ledger pattern is unsuitable when a DNS platform provides a proven atomic multi-record transaction whose semantics and audit export meet your requirements. Use that native primitive when it is stronger than an application worker. It is also a poor fit for sub-second traffic steering; weighted routing or an application-level router belongs in that control plane.

For a one-off internal hostname, a manual change may be reasonable, provided it is clearly outside the tenant-facing release promise. This approach is not suitable when the team cannot retain an approval and observation trail; choose a provider-native transaction or a controlled change-management system instead. Do not apply the manual shortcut to marketplace storefronts, where authorization, rollback lineage, and evidence are part of the product contract.

The decision rule is narrow: if two names must move together, store one intent, publish with stable idempotency keys, verify both authoritative answers, and retain enough evidence to explain a rollback. The runtime can be Node.js, Go, or another service; the ledger semantics are the durable part.

References

Further reading

Top comments (0)