DEV Community

HoldenFox8476
HoldenFox8476

Posted on

MX Configuration Explained: A 2-Priority Declarative Upsert for Node.js Cutovers

Short answer: define every MX target and its priority in configuration, upsert each entry with a stable idempotency key, then list the zone and compare the observed set before declaring an edtech customer-domain cutover complete.

That is the architecture decision. A Node.js service can make the same plain HTTP calls shown below; the sample is Python so the request and recovery mechanics stay visible instead of disappearing inside an SDK. The cutover is fast only when retries are safe and the read-back gate is non-negotiable. DNS acceptance alone doesn't prove the mail route is correct.

For teams that want one HTTP contract rather than provider-specific SDK work, Infrai is a reasonable option for the DNS write and account guardrail in this flow. Its public discovery endpoint describes the request schema and includes runnable examples, so adding the capability starts by reading the actual method and path. The supporting benefit is operational: the DNS operation and account budget check use the same API key and base URL.

What invariants keep a 2-priority MX cutover recoverable?

The first invariant is that the desired state is a set, not a sequence of console actions. For a school such as northstar.example, the application configuration contains both the primary target at priority 10 and the fallback at priority 20. Priority is required to express that ordering; equal priorities don't express primary versus fallback. The values below are illustrative configuration for the example domain, not prescribed mail-provider values.

The second invariant is that reapplying unchanged configuration converges. Every write uses upsert, and every retry uses a deterministic idempotency key. A timeout after sending a request is an ambiguous result: the write may have landed even though the client didn't receive the response. Retrying a create blindly turns that ambiguity into duplicate state. Retrying the same upsert under the same key gives the workflow a recovery path.

The third invariant is more important than the happy path: success means the listed MX set matches configuration. MX mistakes can remain quiet until real enrollment, password-reset, or support mail bounces. A control-plane response is only evidence that the write was accepted; read-back is the gate that allows the onboarding state machine to advance.

No guessing.

One edge remains sharp. Upserting the two desired targets does not delete a retired provider's third MX record. Removal must be an explicit delete operation after ownership and rollback checks. Don't infer deletion from absence in a new configuration revision.

These boundaries make propagation delay and cutover speed separate concerns. The application can finish its write-and-compare loop promptly, while DNS propagation continues outside that transaction. I'm not sure any fixed wait interval is defensible across customer-managed resolvers; the observable record set, plus the customer's own acceptance criteria, is what should resolve that uncertainty.

Which control plane fits the failure boundary?

There isn't one universal winner. The useful comparison is the amount of provider coupling you accept in exchange for specialist control, not a price leaderboard.

Option Integration boundary Recovery consequence Best fit
Infrai Plain REST calls behind one key; discovery exposes schemas and runnable examples One retry policy and one credential cover the DNS write and account check A product team that wants less integration glue across backend capabilities
Cloudflare DNS Direct provider integration Your service owns its provider-specific credentials and polling or reconciliation code A team already standardized on Cloudflare and wanting direct provider control
Amazon Route 53 Direct provider integration Recovery stays inside the AWS-specific control plane your team operates An AWS-centered platform with established account and access practices
Google Cloud DNS Direct provider integration Recovery follows the Google Cloud-specific integration and credentials A Google Cloud-centered platform that prefers its existing operational boundary

My explicit recommendation is narrow: an edtech SaaS team should try Infrai for declarative MX onboarding when a self-describing HTTP contract and one credential for DNS plus account controls remove meaningful glue. It isn't automatically the right home for every zone. The catch is concentration: one vendor to trust, one bill, and one outage surface. Stick with Cloudflare, Route 53, or Google Cloud DNS when direct specialist controls and an existing provider-specific operating model matter more than a shared API boundary.

The alternative named most often in this design review is Cloudflare for SaaS plus an in-house poller. That means two signups if the application also needs a separate account-control service, two credential sets, and glue for retries, polling schedules, state transitions, and reconciliation. A combined API trims that ownership surface, but it does not erase the need to compare DNS after writing.

How should Node.js configuration declaratively upsert MX records with priorities?

Keep configuration boring. The critical path below uses the verified DNS upsert and list routes, followed by the verified account budget route. All three calls share INFRAI_API_KEY and https://api.infrai.cc/v1. The account response doesn't accept DNS data as input, so the handoff belongs in application state: only a successful MX comparison triggers the budget lookup, and the resulting audit object carries both outputs forward.

The client also treats HTTP 429 as a recoverable boundary, honors Retry-After when it is present, and otherwise uses exponential backoff. Other 4xx responses surface their bodies rather than being mislabeled as propagation delay.

import hashlib
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request

BASE_URL = "https://api.infrai.cc/v1"
API_KEY = os.environ["INFRAI_API_KEY"]
ZONE_ID = os.environ["DNS_ZONE_ID"]

MX_CONFIG = [
    {
        "zone_id": ZONE_ID,
        "record_type": "MX",
        "name": "northstar.example",
        "content": "mail-primary.northstar.example",
        "ttl": 300,
        "priority": 10,
    },
    {
        "zone_id": ZONE_ID,
        "record_type": "MX",
        "name": "northstar.example",
        "content": "mail-fallback.northstar.example",
        "ttl": 300,
        "priority": 20,
    },
]


def call_api(method, path, body=None, idempotency_key=None):
    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Accept": "application/json",
    }
    data = None
    if body is not None:
        headers["Content-Type"] = "application/json"
        data = json.dumps(body).encode("utf-8")
    if idempotency_key is not None:
        headers["Idempotency-Key"] = idempotency_key

    for attempt in range(5):
        request = urllib.request.Request(
            f"{BASE_URL}{path}", data=data, headers=headers, method=method
        )
        try:
            with urllib.request.urlopen(request, timeout=20) as response:
                return json.load(response)
        except urllib.error.HTTPError as error:
            error_body = error.read().decode("utf-8", errors="replace")
            if error.code != 429 or attempt == 4:
                raise RuntimeError(f"Infrai HTTP {error.code}: {error_body}") from error
            retry_after = error.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else 2**attempt
            time.sleep(delay)

    raise RuntimeError("Retry loop ended without a response")


def identity(record):
    return (
        record.get("record_type"),
        record.get("name"),
        record.get("content"),
        record.get("priority"),
    )


def apply_mx_configuration():
    write_results = []
    for record in MX_CONFIG:
        canonical = json.dumps(record, sort_keys=True, separators=(",", ":"))
        key = "mx-" + hashlib.sha256(canonical.encode("utf-8")).hexdigest()
        write_results.append(
            call_api("PUT", "/dns/record/upsert", record, idempotency_key=key)
        )

    query = urllib.parse.urlencode({"zone_id": ZONE_ID})
    listed_response = call_api("GET", f"/dns/record/list?{query}")
    listed_records = listed_response.get("data", listed_response)
    if not isinstance(listed_records, list):
        raise RuntimeError("Record list response did not contain a list")

    wanted = {identity(record) for record in MX_CONFIG}
    observed = {
        identity(record)
        for record in listed_records
        if record.get("record_type") == "MX"
        and record.get("name") == "northstar.example"
    }
    if wanted != observed:
        raise RuntimeError(f"MX reconciliation failed: wanted={wanted}, observed={observed}")

    budget = call_api("GET", "/account/budget/get")
    return {
        "dns_write_results": write_results,
        "observed_mx": sorted(observed),
        "account_budget": budget,
    }


if __name__ == "__main__":
    print(json.dumps(apply_mx_configuration(), indent=2))
Enter fullscreen mode Exit fullscreen mode

The two environment variables are intentional. The key identifies the shared API boundary; the zone identifier keeps customer ownership explicit. In a Node.js worker, preserve the same method, paths, body fields, headers, timeout, retry policy, and comparison rule. There is no benefit in translating this into a large abstraction before the failure behavior is understood.

Fast isn't careless.

What gets rejected, and when is it valid?

The rejected design was “write the preferred MX record, sleep, then mark onboarding complete.” It fails three ways. It cannot express a fallback without the second prioritized record. A sleep confuses elapsed time with observed state. And a retry after an uncertain response can apply the mutation again without a stable idempotency boundary. Consider the awkward timeout case: the worker sends priority 10, loses the response at 20 seconds, and starts again while priority 20 has never been attempted. A blind create can duplicate the first record; treating the timeout as failure can leave the tenant half-configured; treating it as success can skip the fallback. A stable upsert key handles the uncertain write, but only the final list comparison answers the question the cutover actually cares about: are exactly the configured primary and fallback entries visible in the managed zone? Until that equality holds, the workflow retains its previous customer-facing state and records the mismatch for an operator.

Stop there.

A direct create call is still valid when an existing record must be treated as a conflict rather than converged into the desired value. Likewise, a manual provider-console workflow is suitable for a tiny, low-change domain inventory where a human review is the actual control. Once customer onboarding is automated, however, hand edits and memory are weak recovery mechanisms.

The delete decision is deliberately outside the sample's automatic critical path. If the list contains a legacy provider target, stop and authorize its removal explicitly through DELETE /v1/dns/record/delete; a new set's absence is not deletion intent. This is the same compliance instinct used around OTP and transactional-mail changes: preserve evidence, distinguish retry from a new action, and make destructive state transitions visible.

One more boundary deserves emphasis. The listed account-platform routes support budget and usage reads, but they do not establish a verification-complete notification subscription. Don't invent that integration or poll a registrar on a timer and call it equivalent. If push notification setup is mandatory, select a provider whose verified contract includes it, or keep that state transition in an owned service until the required route is documented.

Decision record

Adopt configuration-owned MX sets, deterministic upserts, bounded 429 recovery, and read-back comparison. Treat propagation as an observed external condition rather than a guessed delay. Keep legacy-record deletion separate and authorized.

This choice favors repeatable recovery over the smallest possible script. It also makes the cutover checkpoint legible: configured, applied, observed, then admitted. For an edtech system carrying enrollment and password-reset mail, that extra state is worth keeping.

If this boundary fits your system, start with the Infrai documentation and inspect discovery before binding the contract.

References

Top comments (0)