Proof you can watch
// MOST VERIFICATION SAYS "CHECK BACK IN 48 HOURS". THIS ONE ASKS YOUR NAMESERVERS AND SHOWS EVERY STEP AS IT LANDS.
DomainClaim lets you claim a domain and prove you control it with one DNS TXT record. Most products stop at "add this record and check back in 48 hours". This one asks the domain’s own nameservers and shows the check as five steps as they land: find the zone, reach the nameservers, find the record, match the value, record the claim.
Every failure has a name, a reason and one next action. Ownership keeps being checked after it is proved: a name whose record stops answering moves to At risk, and back when the record returns.
What goes wrong
I started with the ways verification fails, before any screen. A resolver that looked too early caches "no record" for up to an hour. DNS panels add your domain to the name you type, so the record lands at the wrong name. A value gets pasted with an end missing. The record is saved as the wrong type. A wildcard answers with another service’s record. One nameserver is down. The token runs out.
Each of these needed its own answer, and each one shaped the product.
- IETF domain verification draftA scoped underscore label, the expiry inside the value, and asking the zone’s own servers.
- RFC 2308How long a resolver keeps saying "no record" after it looked too early.
- RFC 8555 DNS-01How certificate authorities prove control of a domain.
- RFC 8552Why an underscore label can’t collide with a real hostname.
- Five productsHow Google, GitHub, Vercel, Atlassian and Cloudflare verify, and what their errors say.
- A deployed probeDirect queries from the host, the doubled name, split TXT values and timeouts, measured.
Ask the source
The check asks the zone’s authoritative nameservers directly. They don’t cache, so a record is found as soon as the DNS host publishes it. I measured that from a deployed function before building on it. There is no "propagation" copy anywhere in the product.
Whose move is next
The first check on every new claim looks for a record nobody has added yet. A red cross there would report the product working correctly as a fault. So every step sorts by whose move is next: yours (amber), time’s (cyan), or done (green). There is no red.
Claim, then prove
Claiming a name and proving control are separate. A pending claim holds nothing, so anyone can start one. One account holds a name at a time, and the database enforces it with a unique index over the holding states, so a race can’t get around it.
WHAT THE CODE DECIDED
WHERE IT CAME FROM
Built twice
I built the engine first: the DNS walk, the typed failures, one owner per name, and a demo namespace where every failure is reachable. The screens got the time that was left, and the stack shaped them into three separate pages.
The first people outside to use it found the problems in the frame. The sign-in link needed a second click, and the layout shifted between screens. Both were on my list, and I had deferred both. So I kept the engine and started the design again from a blank page, in HTML prototypes.
I drew three directions from how DNS fails and where the person acts or waits: a rail with a drawer, a scrolling timeline, and the claim as a patch network. The timeline won. It carried the check as one horizontal row of steps on a single cable.
The next version applied a whole list of changes at once and lost what made the timeline work. From then on each prototype changed one thing, and I decided each change before it was made. The design converged in a day.
Only real states
Before choosing a stack I checked every state in the design against the backend’s real types. Anything the app couldn’t reach was cut or marked as future work. "Just registered" and "no nameservers set" look the same in DNS, so they became one state. A promise in the copy became build work, or the copy changed.
The stack came last, with one rule: pages never load data, and screens fetch from the API. The check streams its real steps. It never animates a sequence over one finished answer.
- Waiting is never drawn as a fault.
- Cables only where there is a sequence, and straight.
- Past cards dim. Future cards are absent.
- The pulse runs only where the check is.
- Copied is a word, never a tick.
- Every state shown is one the app can reach.
- On a phone, a row that scrolls sideways never wraps.
The code is built to be read. DNS sits behind an interface with a scripted fake, so no test touches the network. Failures are values in a typed union, and the message switch is exhaustive: a new failure breaks the build until it has words. An RFC leads the code, and a friction log records every problem I hit using it against real domains. The unit tests cover the pure layers, and the README names what they don’t.
export type FailureReason =
| { code: 'record_not_found'; queriedName: string;
nameservers: string[]; negativeTtlSeconds: number | null }
| { code: 'no_txt_at_name'; queriedName: string }
| { code: 'appended_zone_suspected'; queriedName: string; foundAt: string }
| { code: 'value_mismatch'; expected: string; found: string[] }
| { code: 'token_expired'; expiredAt: Date }
| { code: 'nameservers_unreachable'; attempted: string[]; timeoutMs: number }
| { code: 'zone_not_found'; walked: string[] }; - src/lib/dns/trace.tsAsks DNS and reports what each server said.
- src/lib/claims/evaluate.tsCompares that with the claim and decides what the row should do.
- src/lib/claims/steps.tsTurns a check into the five steps.
- src/lib/claims/messages.tsEvery failure as title, value, action and reason.
- src/lib/dns/testNames.tsScripts the demo names.
DomainClaim is live, and the demo names reach every failure above. Designed and not built yet: checking on a schedule while nobody has the claim open, the grace window and the email behind it, transfers between accounts, and a second vantage point.
▸ VISIT LIVE · domainclaim-pi.vercel.app ↗