PDS over iroh
README.md

local-pds #

A single-user atproto PDS that runs on your own computer, reachable through iroh instead of a public IP, plus a relay that gives it a normal HTTPS address.

 Bluesky / any atproto app ──HTTPS──▶ pds-relay ──iroh (QUIC, NAT-traversing)──▶ local-pds (your laptop)
  • local-pds: the PDS. One account, one file on disk (repo.car, the repo as a CAR v1) plus config.json for keys. Serves XRPC over iroh and on a localhost port.
  • pds-relay: a stateless HTTPS front. It decides which account a request is for (auth token sub, repo/did param, or login identifier), looks up that DID's iroh endpoint in its DID document and forwards the request over iroh. It merges the firehoses of every PDS behind it into one subscribeRepos, so network relays (bsky.network) can crawl it.
  • iroh-http: HTTP/1.1 (and websocket upgrades) over iroh bi-streams, ALPN atproto-xrpc/http1/0.

DID document #

local-pds init creates a did:plc whose services are:

"atproto_pds": {"type": "AtprotoPersonalDataServer", "endpoint": "https://<relay>"},
"iroh_pds":    {"type": "IrohPersonalDataServer",    "endpoint": "iroh://<hex endpoint id>"}

The relay only forwards to a DID whose document lists the relay as its #atproto_pds, and only to the iroh endpoint in #iroh_pds. iroh endpoint ids are public keys, so the relay is talking to the right node.

Use #

cargo build --release
# handle: a domain you control (DNS TXT _atproto.<handle> "did=<did>")
./target/release/local-pds init --handle you.example.com \
    --relay-url https://relay.example.com --password '…'
./target/release/local-pds serve
./target/release/local-pds post "hello from my laptop" \
    --reply-to https://bsky.app/profile/someone.bsky.social/post/3abc…

Data lives in ~/.local-pds (--data-dir / LOCAL_PDS_DIR).

Relay #

pds-relay --public-url https://relay.example.com   # listens on :8080, put TLS in front

deploy/relay has a Dockerfile and fly.toml. On registration the relay asks the --crawlers (default https://bsky.network) to crawl it.

Storage #

repo.car is a valid CAR at all times: a commit appends its blocks, fsyncs, then rewrites the fixed-size header to point at the new commit. A crash leaves only unreferenced trailing blocks; a torn final block is cut off on open, and dead blocks are compacted away on startup. Blobs are stored in the same file as raw blocks. The MST, record index and firehose buffer are in memory; firehose seqs are microsecond timestamps, so they keep increasing across restarts.

Not yet #

OAuth (logging in to bsky.app needs it), app passwords, email, account migration, blob GC for unreferenced uploads while running. See bd ready.

Repo/MST/CBOR code comes from vlatproto (MIT); commit assembly and PLC helpers are adapted from vlpds (MIT).