- Rust 100%
- Docker <1%
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) plusconfig.jsonfor 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/didparam, or loginidentifier), 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 onesubscribeRepos, 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).