Skip to content

repo writes

nate
Jul 9, 20263 min read1 read

repo writes

hat a PDS actually does when a record is written. the repo is a signed content-addressed merkle search tree; every mutation produces a new signed commit (did, version: 3, data MST root CID, rev TID, nullable prev, sig), and paths are exactly <collection>/<rkey> — normalized NSID, valid rkey, two segments, no leading slash. evidence throughout from zds, a zig PDS.

serialize per repo, then profile the phases

writes take a per-DID lane lock (sharded), then a DB transaction wrapping block/record/commit/event inserts — one writer per repo, many repos in parallel. zds profiles the write path by phase (validation / lock_wait / load_repo / stage_records / build_commit / sql / event_publish), which is what makes "where did the write time go" answerable instead of vibes (store.zig:1864,2029).

load the MST lazily

a repo's MST can be big; a write touches a narrow path through it. zds loads the tree lazily against the block store (Mst.loadLazy over a repo_blocks reader) instead of materializing it — the write only reads the blocks its path crosses (store.zig:1896).

revs must increase even when the clock doesn't

rev is a TID derived from wall-clock microseconds, and the spec requires it to be strictly increasing per repo. clocks stall and step backwards, so zds guards: if the clock hasn't advanced past the current rev, force current_timestamp + 1 (revForSeq, store.zig:2068). without this, two writes in the same microsecond (or after NTP adjustment) produce a non-advancing rev and downstream since semantics break.

a block keeps the rev of its first appearance

repo_blocks rows are stamped with the writing commit's rev, preserved on conflict (COALESCE(repo_blocks.repo_rev, excluded.repo_rev), store.zig:1941). a block re-referenced by a later commit keeps its original rev — which is exactly what makes incremental export (getRepo?since=<rev>, see car-export) return the blocks a consumer is missing rather than everything the latest commit touches.

swap preconditions live under the write lock

swapCommit / swapRecord (compare-and-swap on the repo head or a record's CID) are checked inside the same lock and transaction as the write (store.zig:4544) — checking them before taking the lock is a TOCTOU bug.

the write is more than the record

one write, in order: repo-path validation → schema validation where available (see lexicons for what "validation" really means) → MST mutation → signed commit → block storage → firehose event (precomputed frame, see serving-event-streams) → blob reference bookkeeping (blobs) → crawler notification. skipping any step shows up later as a sync or verification failure someone else debugs.

service auth rides on the same keys

getServiceAuth mints a short-lived JWT signed by the account's key — issuer: account DID, audience: service DID, optional lxm binding the token to one method. the receiver verifies against the account's DID document. this is the primitive under PDS-to-PDS and PDS-to-service calls.

sources

  • zdssrc/atproto/store.zig, docs/architecture.md
  • repository spec
  • reference implementations worth reading beside the spec: the official PDS, tranquil-pds, pegasus

Did you enjoy this article?

Recommend it — Standard Reader surfaces well-loved writing to more readers across the network.

Across the AtmosphereDiscussions