Prufy notes
Orientation · 6 min

The big picture

What Prufy is for, how its four parts fit together, and the one idea that organizes all of them.

The problem

Before a product lets you act as a domain, it needs proof you control that domain. Typing the name proves nothing. The standard proof is a DNS record: publish this exact code at this exact name. Only someone who can edit the domain's DNS can do that.

Prufy is a product that does only this. It started as one step inside a made-up email product called Relay, and docs/decisions/0007-prufy-one-product-one-job.md records why the email part was dropped: a tool that verifies a domain can be used for real, and one TXT record is the whole job. There's no sign-up. The first domain you enter signs that browser in with a cookie.

The idea is simple. The work is in the failures: typos, pasting quotes, adding the record at the wrong company, caches that make it look like nothing happened, two accounts racing, and the record quietly disappearing a year later. Most of this codebase exists to handle those well.

The four parts

Click a part to see what it does and what it depends on.

System mapClick any box
coreNo I/O
Every decision. Is this domain acceptable? Is this move legal? What does this evidence mean? What should we say? Is this change worth an alert? All pure functions.
Without it: The rules would be scattered through request handlers, and you'd need a network to test them.
src/classify.ts src/claim.ts src/normalize.ts src/explain.ts src/view.ts src/alerts.ts
Predict it first
Which part could you run on a plane, with no network and no database?

The one idea

One sentence organizes the whole system: dns gathers evidence, core judges it, db remembers the result. Below is a real example of that pipeline. The verdict in the middle is computed on this page by the actual classify function.

Evidence, judgment, memory
dns gathers
ns1.example.net
NOERROR, has the value
google
NOERROR, has the value
core judges
verified
classify(observations, expected, context)
db remembers
checks: outcome
observations: 2 rows
domain_claims: status
claim_events: audit row
Keeping these apart is why the rule can be tested exhaustively with no network, and why a stored check can explain itself months later.

Six ideas that explain most of the code

1
The lifecycle and the display are separate
The database stores what's true about a claim. What you see is worked out fresh on each read, so the two can't disagree.
2
Illegal states can't be written down
Types refuse nonsense at compile time. Constraints refuse it again in the database.
3
Every failure has a name and a fix
Not "failed", but "your domain is in the Name field twice, change it to this".
4
Proof comes from more than one place
A single DNS answer could be forged, so a separate network path has to agree.
5
Verified isn't forever
Domains expire and change hands, so the record stays in DNS and gets re-checked every day.
6
A credential is random, hashed and narrow
Session cookies, setup links, email confirmations and one-click state are all 32 random bytes, stored only as a SHA-256 hash, and each opens as little as possible.