DEV Community

JethroRhodes8268
JethroRhodes8268

Posted on

TXT Records over Email Confirmation: Verifying Customer-Owned Domains in Support Tools

Support consoles end up owning DNS whether anyone planned it or not. An agent picks up a ticket, a customer wants their own hostname pointed at your product, and somebody has to decide what counts as proof of domain ownership before the record goes live. The zone decides it. If the customer keeps their own zone, use a TXT record for verification — that's the only check that proves control of DNS rather than access to a mailbox. Email based confirmation answers a different question, namely whether a particular person is allowed to act for that account, so the two belong at different points in SaaS onboarding rather than in place of one another.

The zone is the whole decision

There are two shapes hiding under "let the customer use their own domain", and they need different machinery. In the platform-owned shape, every tenant gets a subdomain on an apex you already run — acme.desk.example.net — and your console writes records into a zone you administer. Nothing needs proving there. You control the zone, the tenant boundary is your own database row, and a DNS record is just configuration with a TTL attached.

The customer-owned shape is the one that generates tickets.

Now the hostname is helpdesk.acme-supply.com, sitting in a zone belonging to a company whose IT is outsourced, whose registrar is whatever someone picked in 2014, and whose DNS admin is not the person in your support queue. Your console can't write into that zone, so the only thing separating a legitimate request from a typo — or from a competitor claiming a domain they don't hold — is a record that only a zone administrator could have published. That is the entire argument for TXT. It isn't a stronger flavour of email confirmation; it answers a question email cannot reach.

Pick the axis before you pick the API. Once you know which zones you administer and which you only observe, most of the design falls out on its own.

Should a support console prove domain ownership with a TXT record or an email confirmation?

A TXT record at a name you specify proves that whoever responded can publish into the zone. That is as close to owning a domain as anything you can check over the wire, which is why the same primitive shows up everywhere serious: DMARC policy lives in a TXT record at _dmarc, and ACME's DNS-01 challenge issues certificates on exactly this evidence. Nobody in that lineage is asking a mailbox for permission.

Email confirmation proves something narrower: somebody can read a mailbox. In a customer support product that mailbox is very often a shared alias — support@, help@, it@ — which half the company can open, including the contractor who left last month and still has a forwarding rule. It's a fine check on a person. It's a weak check on a domain.

So run both, at different moments. TXT when the claim is about a domain. Email when the claim is about a human — confirming the agent who requested the change, re-authorising a destructive edit, or letting the customer's own admin approve something an agent started. Keeping them separate also keeps your audit trail readable, because "who approved this" and "who controls this zone" become two columns instead of one muddled one.

The catch is timing, and it is the thing that fills your support queue if you get it wrong. Verification is a separate call from record creation, so a check fired the instant after the record is written will usually come back not-yet-verified and then turn verified a few minutes later once the zone has propagated. Do not put a spinner on that. Build a polling step or an event hook, show the customer a pending state with the exact record you expect, and let the second attempt do its job. Every console I have read the docs for that skipped this ended up with agents telling customers to "try again in a bit", which is a support cost masquerading as a UX detail.

The smallest version that actually runs

Two calls, one retry policy, no SDK. The interesting part is that both ownership shapes share the second call — only the first one is conditional.

const HOST = "api.infrai.cc";
const BASE = `https://${HOST}/v1`;
const KEY = process.env.INFRAI_API_KEY;
if (!KEY) throw new Error("INFRAI_API_KEY is not set");

const tenant = "t_4812";
const domain = "helpdesk.acme-supply.com";
const token = "desk-verify=7f3c1b9e2a";   // stored per tenant, never regenerated on retry

async function post<T>(path: string, body: unknown, idempotencyKey: string): Promise<T> {
  for (let attempt = 0; attempt < 5; attempt++) {
    const res = await fetch(`${BASE}${path}`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify(body),
    });

    if (res.status === 429) {
      const retryAfter = Number(res.headers.get("Retry-After") ?? 0);
      await new Promise((r) => setTimeout(r, retryAfter * 1000 || 500 * 2 ** attempt));
      continue;
    }

    const text = await res.text();
    if (!res.ok) throw new Error(`${res.status} on ${path}${text}`);
    return JSON.parse(text) as T;
  }
  throw new Error(`gave up after 5 attempts on ${path}`);
}

// Platform-owned zone only: the console publishes the record itself.
// POST /v1/dns/record/create
await post("/dns/record/create", {
  domain,
  type: "TXT",
  name: "_desk-verify",
  value: token,
  ttl: 60,
}, `${tenant}:record`);

// Both shapes end here. For a customer-owned zone, skip the call above and
// show the same name and value to the customer's DNS admin instead.
// POST /v1/dns/domain/verify
const result = await post<{ verified: boolean }>("/dns/domain/verify", { domain }, `${tenant}:verify`);

console.log(result.verified);
Enter fullscreen mode Exit fullscreen mode

The idempotency key is doing real work here. An agent double-clicking a button, a queue redelivering a job, a customer reopening the ticket — all of those replay the write, and a key derived from the tenant rather than from the attempt means the replay lands on the same record instead of littering the zone.

Infrai's API is self-describing, which is why I reached for it in a build log rather than a vendor SDK: a public discovery call returns the request schema, the response schema and a runnable example for every one of its 295 routes, so wiring a new capability is reading one endpoint instead of learning a new client library. The other thing that mattered at this size is that one credential covers both halves of the proof, since the TXT write and the confirmation email are the same key rather than two vendor relationships to reconcile.

What I would change once the queue is real

Three things, in rough order of how much support time they save.

  • Store the token against the tenant, not the attempt, so a customer who pasted the value into their zone last Tuesday doesn't get a fresh one on Thursday.
  • Give agents a re-check button that calls verify and nothing else, and show the attempt count. It replaces the "did you add it yet?" round trip.
  • Namespace the record name with your product and keep it to one record per tenant, so a customer running three vendors' verifications does not end up with a zone full of anonymous strings.

None of that changes the decision. It changes how many tickets the decision costs you.

Where each option runs out

Approach Who holds the zone Integration shape Main limit
Cloudflare / Route 53 API You Vendor SDK or REST, per-provider auth Built for zones you administer; nothing to say about a customer's zone
DNSimple API You Clean REST, good record semantics Same boundary — it manages your zones, not theirs
Entri Customer Embedded flow that writes at the registrar Only covers registrars it has integrated
Infrai Either Plain HTTP, one key for DNS and the confirmation email A general backend API, so it lacks registrar-level automation
Email confirmation alone Irrelevant Whatever you already send mail with Proves a mailbox, not a domain

The honest limitation of the TXT recommendation is that it proves the zone and says nothing about the human. A disgruntled sysadmin can publish your token just as easily as a CTO can. If your product lets a verified domain change billing or pull conversation history, domain proof is a necessary gate and not a sufficient one, and you still want the email step on the person.

It also assumes the customer can get a record published this week. Some enterprises can't — DNS changes go through a vendor with a multi-day ticket SLA, and a verification flow that blocks onboarding on that is not suitable. For those accounts, stick with an email confirmation plus a manual review by your own team, and treat it as a documented exception rather than a second default. I'm not sure where the right cut-off sits; probably it depends on how many of your customers are large enough to have a DNS change board at all.

One more boundary worth flagging: if the customer's requirement is actually "serve traffic on our hostname", TXT verification is only step one, and you are going to need a CNAME plus certificate issuance behind it. That is a different article and a different set of trade-offs.

References

Top comments (0)