Domain verification that tells you what is wrong, not just that something is.
Companies whose customers configure DNS — email platforms, custom domains, anything with a "verify your domain" screen — rebuild the same brittle checker. This is the part they never get to: the semantics, the provider quirks, and the difference between broken and we could not tell.
MIT-licensed library and CLI. Source-available API. Runs on one small server.
No signup, no key:
curl -s -X POST https://api.propgate.dev/v1/checks \
-H 'content-type: application/json' -d '{"domain":"github.com"}'That returns two findings on a domain nobody would call misconfigured:
spf warn SPF_LOOKUP_LIMIT_NEAR 0 of the ten lookups are left
delegation warn NS_SERIAL_MISMATCH … at 1656468023; … at 1
GitHub's SPF sits at exactly the ten DNS lookups RFC 7208 allows. It works
today, and the next include: anyone adds breaks mail delivery — with no error
at the moment of the edit. Separately, their AWS and NS1 nameservers report
different zone serials, so which answer a customer gets depends on which server
they happen to reach.
Neither is findable with a regex over a TXT record. That is the whole argument.
Quickstart → · API reference → · Every diagnosis code →
- 73 diagnosis codes, each a stable contract you can switch on, each either
reproduced by a fixture in the test harness or carrying a written reason why
it cannot be.
PROVIDER_APPENDED_ZONE_NAMEdeflects a support ticket; "record not found" creates one. - Semantics, not string matching. SPF expanded recursively the way an MTA does it, with the ten-lookup and two-void-lookup limits counted. DKIM keys parsed. The CAA tree climbed per RFC 8659.
- Four verdicts, not two.
pass,warn,fail, andindeterminate— because "this is broken" and "we could not tell" are different answers, and collapsing them is how a monitoring product pages someone at 3am over its own bad second. - The derivation, not just the verdict. Every result carries the lookups behind it: which name, which server, what came back.
@propgate/dns |
Resolver, six evaluators, the taxonomy. Zero runtime dependencies — Node built-ins only |
@propgate/cli |
npx @propgate/cli check example.com |
| propgate.dev | The free public checker |
| api.propgate.dev | Register domains against a versioned profile, verify them, read per-requirement results and a change timeline |
| Self-serve keys | POST /v1/signup then POST /v1/signup/confirm — an emailed code for a key, in two calls and no conversation |
| Continuous monitoring | Domains are re-checked on an adaptive schedule without anyone asking. Consensus across three vantage points, hysteresis before anything is called failed |
| Webhooks | domain.verified, domain.degraded, domain.failed, domain.recovered — signed, Svix-compatible, at-least-once with a queryable delivery ledger |
Verification answers is this domain configured correctly right now. The product is the rest of the lifecycle.
| Status | |
|---|---|
| Verification — checks, taxonomy, CLI, public checker | shipped |
| Self-serve accounts — sign up by email, get a key, manage keys | shipped |
| Registration and on-demand verification via API | shipped |
| Continuous monitoring — sweeper, consensus across vantage points, hysteresis, webhooks | shipped |
Delegation — you delegate pg.example.com to us, one record instead of six |
planned |
| Certificates, Domain Connect | later |
Monitoring was the interesting one, and the hard part was a correctness property rather than a scheduling problem:
Firing
domain.failedbecause one resolver blipped makes our customers page their customers for nothing. That needs consensus across vantage points and consecutive-failure thresholds, withdegradeddistinct fromfailed.
Both are in. A domain is checked from three vantage points concurrently and a
single disagreeing one yields indeterminate rather than a failure; failed
needs consecutive failures, so alternating failure and recovery never reaches it.
The thresholds are still numbers nobody has measured, and the code says so at
every one of them along with what would earn the receipt. state_transitions
stores the per-vantage evidence behind every state change precisely so the first
false alarm can be audited and those numbers can stop being guesses.
docs/DESIGN.md is the scope contract: the problem, why DNS
makes it hard, what is deliberately out of scope, the cost model, and the
roadmap with its gates.
69 of 74 catalogued requirements (93%).
| RFC | Implemented | ||
|---|---|---|---|
| RFC 1034 | Domain names — concepts and facilities | 4 / 4 | 100% |
| RFC 1035 | Domain names — implementation and specification | 5 / 5 | 100% |
| RFC 2181 | Clarifications to the DNS specification | 2 / 2 | 100% |
| RFC 2308 | Negative caching of DNS queries | 2 / 2 | 100% |
| RFC 4035 | Protocol modifications for DNSSEC | 0 / 1 | 0% |
| RFC 4343 | Domain name system case insensitivity clarification | 1 / 1 | 100% |
| RFC 5321 | Simple Mail Transfer Protocol | 1 / 1 | 100% |
| RFC 6376 | DomainKeys Identified Mail (DKIM) signatures | 10 / 10 | 100% |
| RFC 6891 | Extension mechanisms for DNS (EDNS(0)) | 2 / 2 | 100% |
| RFC 7208 | Sender Policy Framework (SPF) | 25 / 28 | 89% |
| RFC 7489 | Domain-based Message Authentication, Reporting and Conformance (DMARC) | 8 / 8 | 100% |
| RFC 7505 | A null MX resource record | 2 / 2 | 100% |
| RFC 8463 | Ed25519 signatures for DKIM | 1 / 1 | 100% |
| RFC 8659 | DNS Certification Authority Authorization (CAA) | 6 / 7 | 85% |
The denominator is our reading of which normative statements apply to a verifier — something that inspects a domain's records and reports on them. It is not a percentage of an RFC's text, which is not a computable number: most of RFC 7208 instructs senders and receiving MTAs, and none of that is ours to implement. Requirements that do not apply are listed in the ledger with a reason and excluded from the denominator, so cataloguing more of what an MTA does cannot improve the figure.
Every requirement marked implemented names a test that must exist and must
assert it; conformance.spec.ts fails the build otherwise. The table is
generated from that ledger and CI rejects the README if it has drifted.
- RFC 4035 §5 — Validating the DNSSEC chain of trust for an answer. We rely on the resolver we query, and read the AD bit it sets. Validating the chain ourselves would mean shipping a trust anchor and a validator, which is Phase 2 work at the earliest — the fixture tier already carries signed, bogus and insecure-island zones for it.
- RFC 7208 §5.5 — Evaluating whether a ptr mechanism matches a given client. Deciding one needs a reverse lookup of the connecting address and a forward confirmation of every name it returns. We report the term as undetermined for a specific sender rather than guessing, which is visible as SPF_IP_UNDETERMINED.
- RFC 7208 §6.2 — Fetching and macro-expanding exp= text on a fail. exp= text is fetched only to build a rejection message after the outcome is already decided, so it changes no verdict. Parsing the modifier is implemented; retrieving and expanding the explanation string is not.
- RFC 7208 §7.3 — Expanding the %{p} macro. It is the validated domain name of the connecting address, which needs the same reverse lookup and forward confirmation as the ptr mechanism. §7.3 advises against publishing it. Reported as unevaluable rather than guessed.
- RFC 8659 §5 — A CA must consider the DNSSEC validation state of the RRset. The DNSSEC state of the CAA RRset is what a CA must consider. We rely on the resolver's validation rather than validating ourselves — see the DNSSEC entries.
pnpm install
pnpm dns:up # NSD + Unbound fixture tier, six roles
pnpm lint # tsc --noEmit across the workspace
pnpm test # static + unit specs, no containers needed
PROPGATE_FIXTURES=1 pnpm test # adds the fixture-backed projectsmacOS needs the override, since only 127.0.0.1 is up on Darwin:
docker compose -f docker-compose.yml -f docker-compose.darwin.yml up -d --wait| Read next | |
|---|---|
docs/DESIGN.md |
The scope contract — why this is hard, what is out of scope, the roadmap |
TESTING.md |
The DNS fixture harness, what it cannot reproduce, and why fileParallelism stays on |
DEPLOYING.md |
Running it yourself: static front ends, one box for the API |
.claude/CLAUDE.md |
The invariants. Each exists because getting it wrong ships something worse than nothing |
packages/dns and the SDKs are MIT — they're the credibility and the top of funnel. The control plane, dashboard, and monitoring scheduler are source-available. The moat is the infrastructure footprint (multi-provider authoritative fleet, multi-vantage-point resolver network), not the source code.