pds #
ATProto Personal Data Server storage.
The AT Protocol is the network behind Bluesky. Each user's data lives there
in a repository kept by a Personal Data Server (PDS): signed records in a
Merkle Search Tree (MST), and binary blobs such as images. To back up,
migrate or inspect a repository you normally go through a running PDS. pds
reads and writes the repository storage itself instead. The tree lives in a
SQLite blockstore, the blobs in a directory next to it, and named refs point
at commits, with head pointing at the current one. Repositories go in and
out as CAR files, the format PDSes use to exchange them.
A few terms recur below. A CID (content identifier) names a block by the hash
of its bytes, with a prefix that says which hash function and which encoding
were used. Here the encoding is DAG-CBOR, a deterministic subset of CBOR in
which a link to another block is a tagged CID. A CAR file starts with a header
that names the root CIDs, followed by the blocks, each with its CID. The
account that owns a repository is named by a DID (decentralised identifier,
W3C DID Core) such as did:web:example.com.
Installation #
Install with opam:
$ opam install nox-pds
If opam cannot find the package, it may not yet be released in the public
opam-repository. Add the overlay repository, then install it:
$ opam repo add samoht https://tangled.org/gazagnaire.org/opam-overlay.git
$ opam update
$ opam install nox-pds
Usage #
Create a repo, put and get records #
let put_and_get ~record_bytes =
Eio_main.run @@ fun env ->
Eio.Switch.run @@ fun sw ->
let fs = Eio.Stdenv.fs env in
let repo =
Pds.v ~sw
Eio.Path.(fs / "my-repo")
~did:(Atp.Did.of_string_exn "did:web:example.com")
in
Pds.put repo ~collection:"app.bsky.feed.post" ~rkey:"abc123" record_bytes;
(match Pds.find repo ~collection:"app.bsky.feed.post" ~rkey:"abc123" with
| Some data -> Fmt.pr "record: %d bytes@." (String.length data)
| None -> Fmt.pr "not found@.");
Pds.close repo
List a collection #
let list_posts repo =
Pds.list repo ~collection:"app.bsky.feed.post"
|> List.iter (fun (rkey, cid) ->
Fmt.pr "%s -> %a@." rkey Atp.Cid.pp cid)
Blobs #
let store_image repo image_bytes =
Pds.put_blob repo ~mime_type:"image/png" image_bytes
Import / export #
let backup repo = Pds.export_car repo
let restore repo car = Pds.import_car repo car
API #
Repository #
Pds.v ~sw path ~didcreates a repository, andPds.open_ ~sw pathopens an existing one.Pds.did tis the DID that owns the repository.Pds.close treleases the database handle.
Records #
Pds.put t ~collection ~rkey datawrites a record and moveshead, both in one transaction.Pds.find t ~collection ~rkeyreturns the record's bytes, if it exists.Pds.delete t ~collection ~rkeyremoves a record.Pds.list t ~collectionreturns the(rkey, cid)pairs of a collection.
Blobs #
Pds.put_blob t ~mime_type datastores a blob and returns itsAtp.Blob_ref.t.Pds.blob t cidreturns the blob stored undercid, if there is one.
Repository state #
Pds.head tis the CID of the current commit, andPds.set_head t cidmoves it.Pds.checkout tis the MST athead.Pds.blockstore tgives access to the blockstore underneath.
Named refs #
A named ref works like a git branch. Pds.ref t name reads one,
set_ref t name cid moves it, delete_ref t name removes it, and
Pds.list_refs t lists them all.
CAR #
Pds.import_car t dataimports every block of a CAR file and returns how many it imported. Either all blocks go in or none do, andheadstays where it was.Pds.export_car twrites every block reachable fromheadas a CAR file.
Storage layout #
<repo>/
├── pds.db # SQLite database
│ ├── blocks table # CID → DAG-CBOR bytes
│ ├── refs table # name → CID (branches)
│ └── meta table # did, version, etc.
└── blobs/ # Large binary data
├── ba/ # First 2 chars of CID
│ └── bafyrei... # Full CID as filename
└── ...
Related Work #
- ocaml-atp provides the ATProto pieces this package is built on: MST, CID, DAG-CBOR and CAR.
- ocaml-sqlite provides the SQLite key-value store under the blockstore.
- Bluesky PDS is the reference implementation, written in TypeScript.
Licence #
ISC. See LICENSE.md.