Twenty crates, shelved by layer. Plate marks run A at the data foundation to G at the tooling edge; within a band the order is the order you would reach for them.
§ 00
Reading the status column
Published version and workspace version are not the same number, and six crates have no published version at all. Every entry states both.
Status marks used in this catalogue
Mark
Means
How you depend on it
Published
On crates.io at 0.14.5, fourteen crates. The git workspace is ahead at 0.15.0-rc.6, unpublished.
name = "0.14.5"
Git only
Never published to crates.io, six crates. atproto-pds and atproto-entryway are publish = false and never will be. atproto-firehose and the two space crates are publishable and have not been released; atproto-oauth-dioxus does not currently build.
name = { git = "https://tangled.org/ngerakines.me/atproto-crates" }
Experimental
The crate says so itself, in its own README — experimental, in development, or no longer maintained. Interfaces and on-disk formats will move.
Pin a revision.
A
Data foundation
Content addressing and the byte formats everything else is expressed in.
The DASL implementation the rest of the workspace is built on. Twelve public modules; nine of them carry the work: cid (three CID types — general, strictly-validated DaslCid, and unvalidated RawCid), drisl (deterministic DAG-CBOR with sorted keys, shortest-form integers, NaN and infinity rejected), car (streaming CAR v1 reader and writer with size and depth limits), storage (block-store trait with memory and disk backends, plus spillover), bdasl (BLAKE3 CIDs for large content, byte-range to chunk mapping, and streaming verification against a sidecar hash tree, so a corrupted chunk is refused as it arrives rather than at the end of the transfer), masl, rasl, tiles and varint. The other three are errors, value and atproto_json, which reads the $link and $bytes sentinels and is where a record’s network CID comes from. car, storage and rasl sit behind the default async feature; turn it off and the encoder comes without tokio.
DID resolution for did:plc and did:web. Any other method is refused by name — did:key included, which encodes a key rather than naming a document, so there is nothing to resolve; a did:key is still read, signed with and verified against. Handle resolution over both DNS TXT and the HTTPS well-known endpoint, with conflict detection when the two disagree; P-256, K-256 and Ed25519 key generation, signing and validation, with SignaturePolicy separating AT Protocol’s low-S rule from the any-S that JWS requires; LRU-cached DID document storage behind a pluggable trait. Resolution takes your HTTP client, reads every response under fixed ceilings of its own, and offers redirect::validated_policy() for a client that follows redirects safely. It also carries the PLC directory client, including audit-log verification and fork reports, both reachable from atpdid plc.
The small, unglamorous pieces every record touches, and the syntax rules that say which strings are well formed: at:// URIs (a DID authority and all three components, as a record address requires), microsecond-precision TIDs, NSID and record-key checks, a Datetime that keeps a format: datetime string verbatim and refuses what the AT Protocol format refuses, the {"$bytes": …} form of binary data, and Rust types for the community lexicons. It performs no I/O in any feature configuration, and a test holds it to that. It computes no CIDs either: that is atproto_dasl::compute_cid_from_json, or atpcid --record.
Creates and verifies record attestations using the CID-first workflow: prepare the record with $sig metadata, serialise to DAG-CBOR, hash to a CID, sign the CID bytes, then either embed the signature (inline) or emit a separate proof record holding a strongRef (remote). Signatures are normalised to low-S form so they cannot be malleated. The repository field inside $sig binds an attestation to one repo, which is what stops a record being lifted into somebody else’s. Verification answers with what it actually verified, and fetches a remote proof at the CID its strongRef pins. atproto-attestation-verify refuses a record given by an AT-URI in some other repository than the one named, which is the copied-record replay in its plainest form.
Two halves, with the resolve feature as the seam. Validation is always there, and on its own with default-features = false — no HTTP client, no DNS resolver: validate_record checks a record against the schema a catalog holds for its NSID, the validate_query_params and validate_procedure_* functions do the same for XRPC calls, and ValidationLimits bounds reference depth so a self-referencing schema costs a rejection rather than a stack. Resolution is the full chain: NSID to a _lexicon.-prefixed DNS name, TXT lookup for the authoritative DID, DID document resolution, PDS endpoint extraction, then the XRPC fetch of the schema itself. The endpoint is checked against the service-endpoint policy before anything is fetched from it, and a schema whose id is not the NSID asked for is refused. Referenced lexicons resolve recursively with a depth limit, fragment-only references against the enclosing lexicon.
The repository layer: the Merkle Search Tree (insert, get, delete, entries, collection listing, diffing for sync, and covering_proof for single-key inclusion proofs, with key height from SHA-256 and a fanout of 4); Commit and UnsignedCommit with their encoding, signing bytes and shape validation; and Repository<S> over any block store, loaded from a CAR and read by RecordPath. Verification checks the commit signature against a supplied #atproto key, the whole tree, an op’s inclusion, or a slice’s integrity; from_car_verified ties a load to an expected DID and always checks CIDs. CAR v1 and block storage are not implemented here — they are atproto-dasl’s, re-exported as the same types, so a repository loads from a CAR with this crate as the only dependency.
Primitives for the 0016 Permissioned Data draft: the SetHash trait with LtHash as the production lattice hash, signed commits that sign only the per-commit context and bind the set-hash digest through an HKDF-keyed HMAC, so a leaked commit stays deniable, a SpaceRepo orchestrator for per-(user, space) record CRUD and a SpaceMembers one for the member list, and the two-step delegation-token to space-credential JWT exchange. A credential lives ten minutes by default and an hour at most, carries a jti its authority can revoke it by, and is bound to a P-256 did:key through RFC 9421 HTTP Message Signatures rather than DPoP. sync writes the repo CAR a space’s getRepo answers with, and verifies one for a syncer. No network code at all.
atproto-space is deliberately network-free, which is the right shape for a server and leaves a consumer with every type needed to read a space and no way to obtain one. This is that missing half, kept a sibling rather than a feature so the no-IO promise stays unconditional. Reading a space takes two calls to two servers — getDelegationToken at the member’s own PDS, getSpaceCredential at the authority — and subscribing takes a third, registerNotify, at the authority. SpaceHosts names both servers, because sending call two to the repo host instead of the authority fails in a way that never mentions hosts. Modules split by who may act: records (writes on an OAuth session, reads on either), sync (the repo host’s side — writer set, signed commit, oplog, CAR, blobs — plus SpaceCheckpoint and catch_up for following a space’s sequenced writers), spaces for the com.atproto.simplespace management surface, and revocation for an authority revoking a credential early.
The whole client flow: PAR, PKCE, DPoP, JWT, scopes
The OAuth crate, and since the removal of atproto-oauth-aip and atproto-oauth-axum the only one you need for a client. Twelve modules. workflow drives the flow end to end — oauth_init pushes the authorization request and returns a request_uri, oauth_complete exchanges the code, oauth_refresh rotates the token — for confidential clients only: every request to the authorization server carries a private_key_jwt assertion, and there is no public-client path. refresh holds RefreshCoordinator, which keeps a client from racing itself into a replayed refresh token and a revoked grant. Underneath, useful to any client and to an authorization server: JWT minting and verification for ES256 and ES256K; JWK generation for P-256 and K-256; PKCE per RFC 7636; DPoP per RFC 9449; resources for RFC 9728 and RFC 8414 discovery; and scopes, a typed parser for the AT Protocol scope grammar. Request storage is a trait with an LRU implementation behind the lru feature.
Components, hooks and server functions that carry the OAuth flow through a Dioxus fullstack application. The only crate directory that is not a workspace member. It is versioned at 0.15.0-alpha.2, built on its own, and ships with no release. It does not currently build: its manifest inherits zeroize from the workspace dependency table, which no longer lists it, so Cargo refuses to parse the manifest.
XRPC over reqwest with three authentication modes (DPoP-bound tokens, app-password bearer tokens, and classic sessions), plus typed repository operations under com::atproto::repo (create_record, put_record, and friends), session creation, refresh and deletion — the last with the refresh token, as the lexicon requires — record fetches by at:// URI optionally pinned to a CID, and URL construction helpers that get the /xrpc/ prefixing right. The bytes helpers, sync CAR reads, session calls and every JSON helper read under a size bound. It re-exports the reqwest it was built against; take your client from there.
Service-auth tokens, minted and verified against a policy
The credential one AT Protocol service presents to another. verify_service_auth resolves the issuer’s DID document, checks the signature against the key that signed it, and checks the token against a ServiceAuthPolicy naming the method (lxm) and audience (aud) the call actually is — so a token minted for another method, or another service, fails here rather than merely needing a valid signature. Minting is the other half, for a service calling out. A pair of functions and a policy type: there is no middleware, and the Axum Authorization extractor this crate used to carry is gone. No binaries of its own; atproto-pds is the worked example now that the hello-world service has been removed.
A terminal client for XRPC calls. It logs in with an app password, keeps the session in a local file, refreshes the access token when it expires and sends the call again, so a script that works today works next month without handling a token. It also does two things a curl line cannot: service proxying through the PDS, and one-off DPoP calls with an OAuth access token it never stores. The crate is a library too — atpxrpc::accounts is the account store, shared with atpmcp so both read and write the same file. clap is a default feature, so cargo install atpxrpc installs the binary; prebuilt macOS and Linux binaries come from the ngerakines/tap Homebrew tap.
A full PDS: the pds server binary and the atproto-pds-admin CLI. It serves com.atproto.repo.*, sync.* (including subscribeRepos, read from a durable sequencer log, and getRepo with ?since= diff slices), server.*, identity.*, admin.*, an OAuth 2.1 authorization server, an account portal and an account-migration wizard, and the simplespace/space permissioned realms. It is the second PDS implementation anywhere to ship Spaces, and the first in Rust. Storage is SQLite — one accounts database, and one file per account — with blob bytes in that per-account file or, behind the object-storage feature, in S3, GCS, Azure or a local directory. Entryway mode, switched on by PDS_ENTRYWAY_URL, puts it behind an entryway (atproto-entryway, or Bluesky’s): it keeps repositories, blobs and signing keys and hands credentials, sessions, OAuth, handles and PLC rotation to the entryway, with the reference PDS’s settings.
Entryway for a cluster of PDS instances ExperimentalGit only
The account side of a PDS cluster, as one service: credentials, sessions, the OAuth authorization server, which PDS each account lives on, service handles, and the PLC rotation key. Each PDS behind it keeps the data side — repositories, blobs and repo signing keys. The contract between the two is the one the reference @atproto/pds implements behind PDS_ENTRYWAY_URL, so it fronts atproto-pds in entryway mode and Bluesky’s PDS alike. It covers signup with well-known and Highport handle backends, legacy sessions as ES256K JWTs a member verifies, delegated account methods, email and password flows, PLC-operation signing, handle changes, an account portal served with no script, reconciliation against PLC, an operation log that retries a member that does not answer, JWT key rotation with an overlap, and adoption of an existing PDS’s accounts. SQLite underneath.
Consumes the JSON view of the network: one WebSocket at /xrpc/network.bsky.jetstream.subscribeEvents, framed per proposal 0015, filtered server-side and already decoded — no CBOR, no CAR, no MST. Automatic reconnection, cursor resumption, Zstandard decompression against Bluesky’s published dictionary, and filtering by collection, DID and kind. Delivery goes through an EventSink you implement, and the cursor moves only once the sink says a write is durable, so a crash re-delivers one frame rather than skipping one; shutdown runs through a CancellationToken, so a consumer drains rather than drops. Behind replay, archive backfills from the Jetstream archive — immutable segment files past the live retention window — and cuts over to the live tail at a pinned tip. Behind verify-cid, each record is re-encoded and checked against the CID the event published. v2 only — the v1 /subscribe endpoint is a different wire and remains served, wire-frozen, by the same hosts; atproto-jetstream = "0.14" keeps a v1 consumer working.
The workspace had a Jetstream consumer, a TAP consumer, and a subscribeReposproducer inside atproto-pds — so every app view that wanted full-fidelity verified events wrote the consumer itself. This is that consumer. wire holds the frame format, the five-member event union, and a two-stage decode that reads the header before paying for the body; verify proves a commit against the repository that claims to have made it, at a level the caller picks (Off; SignatureOnly — the signature, the event’s repo and rev, and every block’s CID; Full, the default — that and the signed MST root and every tracked op’s inclusion in it), and applies the same to the commit a #sync carries; consumer is the WebSocket loop, its reconnect policy, keepalive and idle timeout, and the cursor contract. A commit that fails verification is reported, not dropped.
Streams from a Tap service, which delivers filtered events it has already MST-verified and signature-checked — that work and backfill are Tap’s, not this client’s. Adds at-least-once delivery, acknowledging an event only after the consumer has processed it; exponential backoff; a keepalive that reconnects a connection gone silent; connection failures reported down the event stream rather than only logged, with the fatal ones ending it; TLS; historical events flagged live: false; and an HTTP management API for adding and removing tracked DIDs. Events are Arc-wrapped and small strings are CompactString.
Extracts mentions, URLs and hashtags from plain text and emits facets with correct UTF-8 byte offsets. Implementations written against character indices get this wrong. Optionally resolves mention handles to DIDs during the parse, with a cap on how many identity lookups one text can cost, and leaves out a mention whose handle does not resolve; the CLI names each one it left out on stderr.
A local Model Context Protocol server speaking JSON-RPC 2.0 over stdio, so an assistant can compute a real CID rather than guess at one — and, since it grew past that, resolve identities, fetch records and lexicons, validate XRPC calls against a schema, mint TIDs, parse facets, and move blobs. Twelve tools: create_record_cid, validate_lexicon_schema, resolve_handle_to_did, resolve_identity, parse_facets, get_record, get_lexicon, validate_xrpc, invoke_xrpc, generate_tid, upload_blob and download_blob. Blobs are also readable as resources at atpmcp://public/blob/{did}/{cid}, fetched from the repository’s PDS, verified against the CID, and cached. Two of the twelve write, through the app-password sessions atpxrpc stores: upload_blob only adds, and invoke_xrpc calls whatever NSID it is given, so it is the one tool annotated destructive.
Three crates have been dropped: two in cf7e0aa4, and atproto-oauth-axum in e507a6e6. All three are still resolvable on crates.io at 0.14.5, none of them yanked, so a build that depends on them keeps working — it is just building against code that no longer exists upstream and will not be released again.
What went, and what to use instead
Crate
Was
Replacement
atproto-oauth-aip
The workflow layer above atproto-oauth: oauth_init, oauth_complete, session_exchange.
atproto-oauth, module workflow. oauth_init and oauth_complete live there now, alongside a new oauth_refresh. There is no session_exchange — see the rewritten guide.
atproto-xrpcs-helloworld
A runnable example service demonstrating did:web identity, service documents and JWT auth.
No direct replacement. atproto-pds is the worked example of an XRPC service built on atproto-xrpcs, at considerably larger scale.
atproto-oauth-axum
Axum handlers for an OAuth client’s callback, JWKS and client metadata, and the atproto-oauth-tool binary. Most of its 1,333 lines were defects: the callback was a debug page that returned the access token, the refresh token and the private DPoP key in a plain-text 200.
Nothing replaces the Axum glue, and the Authorization extractor it fronted, in atproto-xrpcs, is gone too. Use atproto-oauth directly — its README carries the client-ID metadata and JWKS rules — and verify service auth with atproto_xrpcs::verify_service_auth and a ServiceAuthPolicy naming the method and audience you expect.