pdscheck #
Live tool: pdscheck.dev
A single-page diagnostic for atproto Personal Data Servers. Drop in a handle, DID, or PDS URL; pdscheck runs a battery of probes against the PDS, against the wider relay network, and against PLC, then tells you what's working, what's broken, and how to fix it.
It's one HTML file. No build, no backend, no analytics, no logging — every probe runs in your browser. If a check fails for CORS reasons, expand the row and run the printed curl from your terminal.
🛠 Pull requests are very welcome. New probes, better remediation copy, additional relays, bug fixes, design tweaks — all of it. See Contributing below. This is a community tool; treat it like one.
What it checks #
The probes run in dependency order; later checks are skipped if earlier ones can't establish the basics.
| # | Probe | What it tells you |
|---|---|---|
| 00 | Resolve target | Whether your handle resolves via DNS, HTTP, or both — and whether the two agree |
| 01 | Reachability | The PDS is up and answering on /xrpc/_health |
| 02 | describeServer |
Server identity, version, registration policy, available domains |
| 03 | OAuth protected resource metadata | RFC 9728 well-known is served and well-formed |
| 04 | OAuth authorization server metadata | RFC 8414 metadata has PAR + DPoP fields |
| 05 | DID → PDS round-trip | The DID document points back at the PDS you're talking to |
| 06 | describeRepo |
The account exists on this PDS, repo is readable |
| 07 | PDS repo status | Repo is active, with a current rev |
| 08 | Relay host status (multi-relay) | Which relays in the network see your PDS as active — with a request crawl button for any that don't |
| 09 | Relay repo sync state (multi-relay) | Per-relay rev comparison against the PDS — flags any relay falling behind |
| 10 | PLC PDS history | Every PDS endpoint this DID has ever lived on |
| 11 | PLC handle history | Every handle this DID has ever used |
The two probes most operators don't realize they need are #00 (dual handle resolution) and #09 (cross-relay rev comparison). The first catches half-migrated handles that show different identities to different clients. The second is a much stronger signal of firehose health than a six-second WebSocket sniff — if relays are current with the PDS, the firehose works; if they're behind, you have a real diagnostic.
Run it locally #
There's nothing to install. Clone, open the file, done.
git clone <this-repo>
cd pdscheck
# any static server works
python3 -m http.server 8080
# or:
npx serve .
Or just open index.html directly. Some browsers restrict fetch() from file:// origins, so a local server is more reliable.
Deploy it #
It's a single file. Anywhere that serves static HTTPS will work — Cloudflare Pages, GitHub Pages, Tangled Pages, Netlify, an S3 bucket with a CloudFront distribution, your own nginx. There's no build step. Drop the HTML, point a domain at it, you're done.
If you want the deployed site to itself participate in atproto identity, serve /.well-known/atproto-did containing your DID alongside the page.
Add your own probes #
The probe registry is a single array near the top of the script. Each probe is an object:
{
id: "my-probe", // unique
name: "Human-readable name",
sub: "method/endpoint description", // shown in muted text next to name
deps: ["resolve"], // skipped if any dep fails
async run(ctx) {
// ctx contains: input, pdsUrl, did, handle, serverDid, pdsRev, ...
// return { status, detail, remediation?, raw?, curl?, subtable?, contribute? }
}
}
Statuses: ok, warn, fail, skip, info. Anything you contribute from a probe (e.g. { pdsRev: "..." }) is merged into ctx and visible to later probes.
If your probe fans out across multiple targets (relays, resolvers, etc.) you can return a subtable: { kind, rows } and add a renderer for that kind. The five existing renderers (resolvers, relay-host, relay-repo, plc-pds, plc-handle) are short and easy to copy.
Contributing #
PRs are welcome and wanted. This tool gets better as more PDS operators bring their own pain points to it. There is no contributor agreement, no CLA, no copyright assignment — see License for why none of that is necessary.
The highest-value contributions, roughly in order:
- New probes that catch a real-world breakage you've actually hit. If you spent two hours debugging something that pdscheck could have flagged in two seconds, that probe belongs in the registry. Bonus points if you bring the remediation copy: what was the actual fix?
- Better remediation copy on existing probes. If a probe's "fix" message wasn't useful when you were stuck, rewrite it. The remediation strings are where this tool lives or dies.
- Additional relays. The relay registry is a single array. If you run a relay or know of one that should be in the public health-check fanout, add it.
- CORS-related fixes. When in-browser probes hit CORS walls, the printed
curlis the escape hatch — but if you find a probe where the curl command is wrong, that's a real bug worth fixing. - Bug fixes, accessibility improvements, mobile layout fixes. All welcome.
Things to keep in mind:
- No build step. No dependencies. This is intentional. The whole tool is one HTML file with vanilla JS. Please don't introduce a bundler, a framework, or an npm dependency without a really compelling reason. If you do want to make that case, open an issue first.
- Probes should be cheap. A probe that takes more than a couple seconds, or that hammers a service, is a bad citizen. Run things in parallel with
Promise.allwhere you can; respect the 6–10 second per-probe timeout budget. - Remediation copy is the product. Anyone can ship a probe that says "✗ failed." A probe that says "✗ failed — your reverse proxy isn't passing /.well-known/* to the PDS, fix it like this" is what makes pdscheck useful. Aim for that bar.
- Be kind to everyone — including LLMs. A lot of contributors will use AI assistants to write their PRs. That's fine and expected. Review the code, make sure it works, and ship it. The point is the tool, not the artisanship.
If you want to discuss a bigger change before writing it, open an issue first. For small fixes, just send the PR.
Provenance & attribution #
pdscheck was created primarily by an LLM (Claude) collaborating with a human (me) who set the goal, picked the probes, made the design decisions, and decided what to merge. The vast majority of the actual code is LLM-generated.
This matters for two reasons:
1. Credit. The probe set and the cross-relay fanout pattern are heavily influenced by debug.hose.cam by fig and bailey, MIT/Apache-2.0 licensed. The implementation here is a vanilla-JS rewrite with different aesthetics and additional probes (OAuth metadata, dual-resolver display, equivalent-curl output, remediation copy), but the design lineage is theirs and they should get the credit. Sponsor them: fig · bailey.
2. Copyright. Under current US Copyright Office guidance (2023, reaffirmed in 2025) and Thaler v. Perlmutter (DC Cir. 2025), purely AI-generated output is not eligible for copyright protection. There may be a thin copyright on human-authored contributions (selection, arrangement, edits, prompts), but rather than rely on that murky boundary, this project is explicitly dedicated to the public domain via CC0 1.0, with MIT as a fallback for jurisdictions that don't fully recognize public-domain dedication.
In plain English: do whatever you want with this. Fork it, sell it, ship it inside something else, rename it, replace the entire UI. No permission needed, no attribution required (though attribution to hose.cam is encouraged because their design did the heavy lifting).
Limitations #
- CORS. Some self-hosted PDSes don't return permissive CORS headers for arbitrary origins. When that happens, in-browser probes fail with opaque errors. The remediation in those cases is the printed
curlcommand — run it from your terminal, that's the source of truth. - WebSocket / firehose checks. Earlier versions of pdscheck included a six-second WebSocket sniff against
subscribeRepos. It's been replaced with cross-relayrevcomparison, which is a stronger signal. - Quiet PDSes. A PDS with no recent activity will show relays as "current" because there's nothing to fall behind on. This is correct, but means pdscheck cannot distinguish "healthy and idle" from "healthy and active."
- Relay list is static. The seven relays in the registry are hardcoded. Add yours via PR.
License #
CC0 1.0 Universal, with MIT as a fallback. See LICENSE.md for the full text and explanation.
Built for self-hosters. If pdscheck saves you from a 3am support thread, that's the whole point. If it doesn't catch the thing that did end up wasting your evening, open a PR and add the probe.