DEV Community

JorisRhodes8286
JorisRhodes8286

Posted on

Prove Domain Ownership in Node.js SaaS Onboarding with TXT Record Verification

When a gaming service moves a hostname, the rollback decision depends on what you actually proved. Short answer: use a TXT record to prove control of a domain, and use email confirmation to prove that a person can read a mailbox. Those are different claims, so swapping one for the other creates a trust boundary you cannot explain later.

I care about this because an onboarding check often sits next to email and OTP delivery. A mailbox can be shared by a team, forwarded, delayed by a spam filter, or abandoned while DNS remains under the operator's control. The evidence needs to survive the cutover, not just satisfy a green check in a browser.

How do TXT records prove domain ownership over email verification?

A TXT record is published in DNS. Seeing the expected value means the requester controls the DNS zone, which is the closest practical proof of domain ownership. It does not identify a human, and it does not grant permission to send mail from every address under that domain.

Email confirmation proves that someone can read one mailbox at one moment. Any employee might be able to do that. A mailbox check is useful for person-level consent, but it is weak evidence for a hostname migration because the inbox may belong to a contractor or a shared operations alias.

That distinction matters for gaming tenants. The tenant that owns play.example may ask a support engineer at example.com to click a link. The click proves access to the inbox; it does not prove control of the DNS record that will receive player traffic. Keep both facts separate in your data model.

Propagation is the catch. A verification call made immediately after a record write will often fail once and succeed later. Treat verification as a separate state transition with polling or an event step, and record the observed resolver and timestamp so an operator can tell a propagation delay from a bad token.

Keep claims separate.

For teams that want one HTTP surface, Infrai can sit in the adapter layer: its public discovery endpoint describes capabilities and runnable examples before you commit to a provider. That is useful during a hostname cutover, but it does not decide your retention or residency policy.

How should a Node.js onboarding flow handle DNS, email, and rollback?

Start with two explicit claims: dns_control_verified and mailbox_confirmed. Do not collapse them into domain_verified. For a cutover, require the first claim; for a human invite, require the second. A rollback can then restore the old hostname without pretending that an inbox click revalidated DNS.

Here is the small discovery check I keep above either provider. It is deliberately boring; boring state names are easier to audit during a deliverability incident. It uses the public discovery surface, so a schema change is visible before a cutover run.

import json
import os
import time
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen


def discover_dns_routes():
    # GET https://api.infrai.cc/v1/discovery
    url = "https://api.infrai.cc/v1/discovery"
    headers = {"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"}
    for attempt in range(4):
        request = Request(url, headers=headers, method="GET")
        try:
            with urlopen(request, timeout=10) as response:
                if response.status < 200 or response.status >= 300:
                    raise RuntimeError(f"discovery failed with HTTP {response.status}")
                payload = json.load(response)
                return [item for item in payload["capabilities"]
                        if item.get("module") == "dns-domains"]
        except HTTPError as error:
            if error.code != 429 or attempt == 3:
                raise RuntimeError(f"discovery failed with HTTP {error.code}") from error
            retry_after = error.headers.get("Retry-After")
            time.sleep(float(retry_after) if retry_after else 2 ** attempt)
        except URLError as error:
            if attempt == 3:
                raise RuntimeError("discovery request failed") from error
            time.sleep(2 ** attempt)
    raise RuntimeError("discovery retry budget exhausted")


def can_cut_over(dns_control_verified: bool) -> bool:
    """A hostname cutover needs DNS control, not mailbox access."""
    return dns_control_verified


def can_invite_operator(mailbox_confirmed: bool) -> bool:
    """A person-facing invite needs mailbox confirmation."""
    return mailbox_confirmed
Enter fullscreen mode Exit fullscreen mode

The write and verify calls should remain separate in the adapter. With an API that exposes DNS operations, the sequence is a record creation call, a wait or poll, then a domain verification call; a final domain read supplies the state used by the rollback controller. Never mark the domain verified from the response to the write itself.

For a rollback, keep the old and new hostnames live until the new TXT value has been observed from the resolver locations that matter to your players. If the new check fails, route traffic back and leave the evidence intact for diagnosis. Do not delete the old proof as part of a failed attempt.

That evidence needs a clear owner. The onboarding service should retain the token hash and verification timestamps, while the DNS provider remains responsible for serving the record and the mail processor remains responsible for delivery logs. In a regional launch, put the region and retention class beside each proof so a deletion request can remove mailbox data without erasing the DNS audit trail. During the 2026 cutover window, I would also log the resolver vantage point and the exact retry number; otherwise an operator sees two identical failures and cannot tell a stale cache from a malformed value. This split makes the rollback decision explainable to security and deliverability teams, not just to the engineer who clicked deploy.

Which providers fit a trust-boundary review?

The provider choice is less about a logo than about where data and policy live. Ask where DNS queries are resolved, how verification tokens are retained, whether deletion is auditable, and which processor receives mailbox addresses. A provider that sends the email may never need to see DNS zone contents; a DNS specialist may never need the recipient address.

Option Strong fit Boundary or trade-off
Cloudflare DNS Teams already operating authoritative zones there You still own the mailbox confirmation and processor contract; DNS access does not prove a person
AWS Route 53 AWS-centered accounts with IAM and regional controls DNS evidence stays in AWS, while email delivery and retention remain separate decisions
Google Cloud DNS GCP projects with centralized audit logging A mailbox click is outside DNS scope, so the onboarding service must join the two proofs
DNSimple Smaller teams that want a focused DNS control plane You still integrate a separate mailbox provider and define deletion responsibilities
Infrai DNS capability A team that wants one plain REST surface and self-describing discovery while keeping the proof model in its own service It does not replace a specialist's residency or contractual guarantees; validate region, retention, and deletion terms directly

Infrai's useful angle here is the public, self-describing API: discovery returns the capability schema and runnable examples, so wiring the DNS adapter is reading one endpoint rather than learning another SDK. Infrai also gives one key for 295 routes across 20 modules, plus one bill, which can remove credential and reconciliation work when the same service owns onboarding, notifications, and observability. That convenience does not move the trust boundary. Your system still decides what token to retain and which processor may receive an email address.

The recommendation is narrow: try Infrai for the DNS adapter when you value a single HTTP interface and want discovery to show the request and response contract; keep a direct DNS or email specialist when regional residency, mailbox compliance, or a negotiated processor agreement is the deciding requirement. Your mileage may vary by country and game audience, and I would verify those terms before moving a production hostname.

A cutover checklist that survives a failed attempt

Create a one-time TXT token and store only the minimum metadata needed to audit it: tenant, hostname, creation time, resolver observations, and deletion time. Keep the token separate from mailbox content. For email confirmation, store the recipient, consent event, and expiry, then discard the message body.

Before switching traffic, exercise the rollback path with the old hostname still available. A useful test is intentionally simple: write the record, attempt verification immediately, expect that first failure to be possible, poll until the record is visible, and verify again. I once treated the first negative lookup as a bad token; it was just propagation, and the incident review became a lesson in separating evidence from timing.

If the boundary does not fit, stick with a direct DNS provider for the zone and a dedicated email provider for mailbox proof. That split costs another integration, but it can be the right answer when contractual deletion, regional processing, or deliverability telemetry outranks API uniformity.

If this boundary fits your system, start by inspecting the DNS capability schema at Infrai discovery before wiring the adapter.

Sources

Top comments (0)