- Rust 98%
- HTML <1%
- JavaScript <1%
- CSS <1%
- Docker <1%
- 1 more language <1%
AT Protocol Rust Crates #
A Rust workspace of crates for building AT Protocol applications. This collection provides building blocks for content-addressed data, identity management, repository operations, record signing, OAuth 2.0 authentication flows, HTTP client operations, XRPC services, real-time event streaming, and developer tooling.
Origin: Parts of this project were extracted from the open-source smokesignal.events project, an AT Protocol event and RSVP management application. This library is released under the MIT license to enable broader AT Protocol ecosystem development.
Components #
This workspace publishes 17 crates that work together to provide complete AT Protocol application development capabilities. Which binaries each one ships, and how to run them, is in Command Line Tools.
Data Foundations #
atproto-dasl- DASL (Data-Addressed Structures & Links) framework implementing CID computation, DRISL (deterministic DAG-CBOR encoding), CAR v1 archives, block storage backends, RASL retrieval, BDASL large file hashing, and Web Tiles.
Identity & Cryptography #
atproto-identity- Core identity management with DID resolution for thedid:plcanddid:webmethods, DNS/HTTP handle resolution, PLC directory operations, and P-256, K-256 and Ed25519 cryptographic operations.atproto-attestation- CID-first attestation utilities for creating and verifying cryptographic signatures on AT Protocol records, supporting both inline and remote attestation workflows.atproto-record- Record types and the encodings records are built from: TID generation, AT-URI, NSID and record-key parsing, datetime formatting,$byteshandling, and the community lexicon types. It computes no CIDs; a record's CID isatproto_dasl::compute_cid_from_jsoninatproto-dasl.atproto-lexicon- Lexicon schemas for AT Protocol, in two halves: a validator that checks records, query parameters and procedure bodies against a schema catalog, and DNS-based resolution that fetches those schemas by NSID. The validator is available withdefault-features = false, without an HTTP client or DNS resolver.
Repository #
atproto-repo- AT Protocol repository handling with Merkle Search Tree (MST) encoding/decoding, commit structures, tree diffing for sync, and configurable verification. Builds onatproto-daslfor CAR and storage.
Authentication & Authorization #
atproto-oauth- OAuth 2.0 with AT Protocol security extensions: DPoP (RFC 9449), PKCE (RFC 7636), JWT operations, and secure storage abstractions. Itsworkflowmodule serves confidential clients only — every authorization-server request carries aprivate_key_jwtassertion — whilepkce,dpop,jwt,jwk,resourcesandscopesare useful to any client, and to an authorization server.
Client & Service Development #
atproto-client- HTTP client library supporting multiple authentication methods (DPoP, Bearer tokens, sessions) with native XRPC protocol operations and repository management.atproto-xrpcs- Minting and verifying service-auth tokens, the credential one AT Protocol service presents to another, against a policy naming the method and audience the call actually is. It is a pair of functions and a policy type: there is no middleware and there are no extractors.atpxrpc- XRPC CLI client with persistent session management for making authenticated AT Protocol API calls.
Atproto Spaces (permissioned data) #
atproto-space- Primitives for the 0016 Atproto Spaces draft: SetHash commitments, signed space commits, the delegation-token and space-credential JWTs (ten minutes by default, an hour at most), the RFC 9421 HTTP Message Signatures that bind a credential to its holder's key, and repo CAR serialization and verification for the space sync surface.atproto-space-client- XRPC client for permissioned-data spaces: the 0016 credential exchange with signed requests, client attestation, space management, the record and sync surfaces, following a space's sequenced writer set from a checkpoint, and credential revocation.
Real-time Event Processing #
atproto-firehose-com.atproto.sync.subscribeReposconsumer: frame decoding, commit verification, and a reconnecting stream.atproto-jetstream- Jetstream v2 consumer: filtered, pre-decoded events over one WebSocket, with cursor resumption, reconnection and dict-zstd compression.atproto-tap- Tap (Bluesky's atproto sync utility) service consumer for filtered, verified AT Protocol repository events, with acknowledgment-based delivery; MST integrity checks and backfill are the Tap service's own work, not this client's.
Developer Tools #
atpmcp- An MCP (Model Context Protocol) server speaking the 2026-07-28 revision over stdio, exposing twelve tools — CIDs, TIDs, facets, identity resolution, record and lexicon fetches, XRPC validation and invocation, and blob transfer — plus blobs as resources. Two of the twelve write, using app-password sessions shared withatpxrpc;invoke_xrpccalls whatever NSID it is given and is marked destructive.
Utilities #
atproto-extras- Facet parsing and rich text utilities for AT Protocol, extracting mentions, URLs, and hashtags from plain text with correct UTF-8 byte offset calculation.
atproto-oauth-dioxus, the Dioxus fullstack integration for the OAuth flow, is in this repository but not a member of this workspace and does not ship with these releases. It is unmaintained and does not currently build: its manifest inherits zeroize from the workspace's [workspace.dependencies], which no longer lists it, so Cargo refuses to parse the manifest.
atproto-pds, a Personal Data Server built from the crates above — the accounts database and per-actor stores, the OAuth authorization server, the account portal with passkeys, the account-migration wizard and the space host — is a workspace member, built and tested with the rest, but it is not published. Neither the crate nor its pds and atproto-pds-admin binaries go to crates.io: build them from this repository, or from the container images described in its README.
Quick Start #
This is exactly what the five examples below need, and nothing else — add the rest from Components as you reach for them:
[dependencies]
atproto-attestation = "0.15.0-rc.6"
atproto-client = "0.15.0-rc.6"
atproto-identity = "0.15.0-rc.6"
atproto-lexicon = "0.15.0-rc.6"
atproto-oauth = "0.15.0-rc.6"
atproto-xrpcs = "0.15.0-rc.6"
axum = "0.8"
chrono = "0.4"
serde_json = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
reqwest is deliberately not in that list. Every crate here whose API takes
a reqwest type re-exports the reqwest it was compiled against —
atproto_identity::reqwest, atproto_lexicon::reqwest, atproto_oauth::reqwest,
atproto_client::reqwest and the rest — and takes a client of that one. A
reqwest from an incompatible release line in your own Cargo.toml, 0.12
against this workspace's 0.13, is a different crate with a different Client
type, and the mismatch reads as "expected Client, found Client". Construct
clients through the re-export, as every example below does, and the versions
never have to be matched by hand.
Basic Identity Resolution #
Resolving a handle or a did:web fetches from a host the identifier names, so
from whoever supplied it. The crate checks the URL it builds, and that covers
the first request only: the client has to refuse redirects, or check every
hop, since reqwest's default follows them to http:// and address literals.
The examples here use Policy::none(), the simple, safe choice; it fails a
handle whose /.well-known/atproto-did redirects, which the handle
specification allows, and atproto_identity::redirect::validated_policy(),
which follows at most five hops and checks each with
validate_service_endpoint, resolves those as well. Response bodies are read
under fixed ceilings of the crate's own, so a host cannot choose how much is
held. atproto-identity's crate docs, under "The HTTP client you pass in", show
both policies and give the ceilings.
use std::time::Duration;
use atproto_identity::reqwest::{Client, redirect::Policy};
use atproto_identity::resolve::{HickoryDnsResolver, resolve_subject};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let http_client = Client::builder()
.redirect(Policy::none())
.timeout(Duration::from_secs(10))
.build()?;
// An empty slice means the system resolvers. Pass addresses to override
// them: `create_resolver(&["1.1.1.1".parse()?])`.
let dns_resolver = HickoryDnsResolver::create_resolver(&[]);
let did = resolve_subject(&http_client, &dns_resolver, "alice.bsky.social").await?;
println!("Resolved DID: {did}");
Ok(())
}
Lexicon Resolution #
use std::sync::Arc;
use atproto_identity::resolve::{HickoryDnsResolver, InnerIdentityResolver};
use atproto_lexicon::reqwest::{Client, redirect::Policy};
use atproto_lexicon::resolve::{DefaultLexiconResolver, LexiconResolver};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Refuses redirects, as in Basic Identity Resolution above.
let http_client = Client::builder().redirect(Policy::none()).build()?;
let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&[]));
// The identity resolver names the PLC directory lexicon authorities are
// resolved against; point it at your own by changing `plc_hostname`.
let identity_resolver = Arc::new(InnerIdentityResolver {
dns_resolver: dns_resolver.clone(),
http_client: http_client.clone(),
plc_hostname: "plc.directory".to_string(),
});
let resolver = DefaultLexiconResolver::new(http_client, dns_resolver, identity_resolver);
// Resolve a lexicon schema
let lexicon = resolver.resolve("app.bsky.feed.post").await?;
println!("Lexicon schema: {}", serde_json::to_string_pretty(&lexicon)?);
Ok(())
}
Record Signing #
use atproto_attestation::{AnyInput, create_inline_attestation};
use atproto_identity::key::{KeyType, generate_key, to_public};
use serde_json::json;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// A deployed service loads its key from configuration with
// `identify_key("did:key:z...")`; generating one keeps this runnable.
let key = generate_key(KeyType::P256Private)?;
let record = json!({
"$type": "app.bsky.feed.post",
"text": "Hello AT Protocol!",
"createdAt": "2024-01-01T00:00:00.000Z"
});
// `key` is the *public* form of the key that signs, so a verifier can find
// it. A placeholder here is not a cosmetic problem: signing reads this
// field and fails on anything that is not a real did:key.
let metadata = json!({
"$type": "com.example.inlineSignature",
"key": to_public(&key)?.to_string(),
"issuer": "did:plc:issuer123",
"issuedAt": "2024-01-01T00:00:00.000Z"
});
// The repository DID is bound into the signature, so a signed record
// lifted into another repository does not verify there.
let signed_record = create_inline_attestation(
AnyInput::Serialize(record),
AnyInput::Serialize(metadata),
"did:plc:repo123",
&key,
)?;
println!("Signed record: {}", serde_json::to_string_pretty(&signed_record)?);
Ok(())
}
XRPC Service #
An XRPC method that accepts inter-service calls verifies the bearer token against a policy naming the method it serves and the audience it answers to. The policy is what makes a token minted for another method, or for another service, fail here — a signature check on its own does not.
use std::sync::Arc;
use atproto_identity::resolve::{HickoryDnsResolver, InnerIdentityResolver};
use atproto_identity::reqwest::{Client, redirect::Policy};
use atproto_identity::traits::IdentityResolver;
use atproto_xrpcs::{ServiceAuthPolicy, verify_service_auth};
use axum::extract::State;
use axum::http::{HeaderMap, StatusCode};
use axum::{Json, Router, routing::get};
use serde_json::json;
/// This service's own identity, and the resolver it checks callers with.
#[derive(Clone)]
struct AppState {
service_did: String,
resolver: Arc<dyn IdentityResolver>,
}
async fn handle_hello(
State(state): State<AppState>,
headers: HeaderMap,
) -> Result<Json<serde_json::Value>, StatusCode> {
let token = headers
.get("authorization")
.and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer "))
.ok_or(StatusCode::UNAUTHORIZED)?;
// `lxm` is the method being served and `aud` is this service. Verification
// returns the claims or an error; there is no third state in which an
// unverified caller reaches the handler.
let policy = ServiceAuthPolicy::new("com.example.hello", &state.service_did);
let claims = verify_service_auth(token, state.resolver.as_ref(), &policy, None)
.await
.map_err(|_| StatusCode::UNAUTHORIZED)?;
Ok(Json(json!({ "message": format!("Hello, {}!", claims.iss) })))
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// A caller's token names the DID resolved here, so its did:web host is
// theirs to choose: the client refuses redirects.
let http_client = Client::builder().redirect(Policy::none()).build()?;
let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&[]));
let state = AppState {
service_did: "did:web:service.example".to_string(),
resolver: Arc::new(InnerIdentityResolver {
dns_resolver,
http_client,
plc_hostname: "plc.directory".to_string(),
}),
};
let app = Router::new()
.route("/xrpc/com.example.hello", get(handle_hello))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
axum::serve(listener, app).await?;
Ok(())
}
Minting the other side of that call is atproto_xrpcs::mint_service_auth.
OAuth Client Flow #
This is a confidential client: OAuthClient carries a private signing key,
and every authorization-server request is authenticated with a
private_key_jwt assertion made from it. A browser SPA or a native app cannot
keep that key from its user, so it cannot drive this flow directly — run it in a
backend that holds the key.
The flow is two HTTP requests in your application, not one: the first serves
the sign-in link, the second handles the redirect back. What carries between
them is an OAuthRequest, which you store keyed by its state — so the two
halves are two functions here.
The state is not a CSRF defence on its own. The library sends it and
never checks it, and a store keyed by state alone will find the request
behind any callback URL, including one an attacker began for their own account
and sent to someone else. Two things make it one, and both are yours to do:
- On the redirect out, set the same value in an
HttpOnly; Secure; SameSite=Laxcookie. - On the callback, refuse unless that cookie equals the query's
state. Then load the request and delete it before exchanging the code, so it is used once.
use atproto_identity::key::{KeyType, generate_key, identify_key, to_public};
use atproto_identity::traits::IdentityResolver;
use atproto_oauth::pkce;
use atproto_oauth::reqwest::Client;
use atproto_oauth::resources::{oauth_authorization_server, pds_resources};
use atproto_oauth::workflow::{
OAuthClient, OAuthRequest, OAuthRequestState, TokenResponse, generate_state, oauth_complete,
oauth_init,
};
type Error = Box<dyn std::error::Error>;
/// An account's DID and PDS, from its DID document. The endpoint is checked
/// against the service-endpoint policy before anything is fetched from it.
async fn did_and_pds(
resolver: &dyn IdentityResolver,
subject: &str,
) -> Result<(String, String), Error> {
let document = resolver.resolve(subject).await?;
let pds = document
.pds_endpoints_validated()
.first()
.ok_or("the DID document names no usable PDS")?
.to_string();
Ok((document.id, pds))
}
/// First request: resolve the account, push the authorization request, and
/// build what the callback will need.
///
/// Returns the URL to send the browser to, and the `OAuthRequest` to store under
/// its `oauth_state`. Set that state in the cookie on the same redirect.
async fn begin_sign_in(
http_client: &Client,
resolver: &dyn IdentityResolver,
oauth_client: &OAuthClient,
handle: &str,
) -> Result<(String, OAuthRequest), Error> {
// The flow is pinned to a DID, never a handle: `oauth_complete` requires
// the token endpoint's `sub` to equal it exactly.
let (did, pds) = did_and_pds(resolver, handle).await?;
let (_protected_resource, authorization_server) = pds_resources(http_client, &pds).await?;
// All three are new for every sign-in. The verifier stays here and only
// its challenge goes on the wire; the DPoP key binds this flow's tokens.
let (pkce_verifier, code_challenge) = pkce::generate();
let dpop_key = generate_key(KeyType::P256Private)?;
let request_state = OAuthRequestState {
state: generate_state(),
code_challenge,
scope: "atproto transition:generic".to_string(),
};
let par_response = oauth_init(
http_client,
oauth_client,
&dpop_key,
Some(handle),
&authorization_server,
&request_state,
)
.await?;
let authorization_url = format!(
"{}?client_id={}&request_uri={}",
authorization_server.authorization_endpoint,
oauth_client.client_id,
par_response.request_uri
);
let now = chrono::Utc::now();
let oauth_request = OAuthRequest {
oauth_state: request_state.state,
subject: did,
issuer: authorization_server.issuer.clone(),
pkce_verifier,
signing_public_key: to_public(&oauth_client.private_signing_key_data)?.to_string(),
dpop_private_key: dpop_key.to_string(),
created_at: now,
expires_at: now + chrono::Duration::seconds(par_response.expires_in as i64),
};
Ok((authorization_url, oauth_request))
}
/// Second request: the callback, carrying `?code=…&state=…&iss=…`.
///
/// By the time this runs the callback has passed the cookie check, and
/// `oauth_request` has been loaded by its `state` and deleted from storage.
async fn finish_sign_in(
http_client: &Client,
oauth_client: &OAuthClient,
oauth_request: &OAuthRequest,
code: &str,
callback_issuer: Option<&str>,
) -> Result<TokenResponse, Error> {
// The authorization server again, fetched from the issuer the flow began
// with. Not rediscovered from the account's PDS: that finds whichever
// server it names now, and `oauth_complete` refuses metadata whose issuer
// is not `oauth_request.issuer`.
let authorization_server = oauth_authorization_server(http_client, &oauth_request.issuer).await?;
// The same DPoP key the flow began with, read back out of the request.
let dpop_key = identify_key(&oauth_request.dpop_private_key)?;
// Two checks happen in here. The callback's `iss` must be the issuer the
// flow began with (RFC 9207), and the token endpoint's `sub` must be the
// DID it began for. Neither is the `state` check above.
let token_response = oauth_complete(
http_client,
oauth_client,
&dpop_key,
code,
&oauth_request.subject,
callback_issuer,
oauth_request,
&authorization_server,
)
.await?;
Ok(token_response)
}
fn configure() -> Result<OAuthClient, Error> {
Ok(OAuthClient {
redirect_uri: "https://your-app.example/callback".to_string(),
// An atproto `client_id` is the URL its metadata document is served at.
client_id: "https://your-app.example/client-metadata.json".to_string(),
// Loaded from configuration in a deployed service, with
// `identify_key("did:key:z...")`.
private_signing_key_data: generate_key(KeyType::P256Private)?,
})
}
The access token that comes back is DPoP-bound, so PDS calls are made with
atproto_client::client::dpop_call and a DPoPAuth holding that flow's DPoP
key, the one stored in its OAuthRequest:
use atproto_client::client::{DPoPAuth, dpop_call};
use atproto_client::reqwest::{Client, Method, header::HeaderMap};
use atproto_identity::key::KeyData;
async fn list_posts(
http_client: &Client,
dpop_key: KeyData,
access_token: String,
did: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let dpop_auth = DPoPAuth {
dpop_private_key_data: dpop_key,
oauth_access_token: access_token,
};
let url = format!(
"https://pds.example.com/xrpc/com.atproto.repo.listRecords\
?repo={did}&collection=app.bsky.feed.post"
);
let response = dpop_call(
http_client,
&dpop_auth,
Method::GET,
&url,
None,
&HeaderMap::new(),
)
.await?;
if let Some(body) = response.body {
println!("{}", serde_json::to_string_pretty(&body)?);
}
Ok(())
}
Refreshing an access token is atproto_oauth::workflow::oauth_refresh, behind
atproto_oauth::refresh::RefreshCoordinator — a refresh token rotates on use,
so two concurrent refreshes of one session will invalidate it unless they are
serialized. Key the coordinator by session, not by DID, since one account
signed in on two devices holds two grants. The future it runs has to read the
refresh token from storage and save the new tokens before resolving, and fails
with the caller's own error type, so a store failure comes back as itself;
RefreshCoordinator::refresh's documentation shows how.
Command Line Tools #
The workspace ships command-line tools and server binaries across the crates, ready-to-use utilities for AT Protocol development and testing. Every binary below is behind the clap feature except atpmcp, which is a binary crate; some need hickory-dns for DNS resolution, and the two attestation tools need atproto-attestation's tokio feature for their HTTP client. One line builds them all:
# Build every binary in the workspace
cargo build --bins -F clap,hickory-dns,atproto-attestation/tokio
cargo build --features clap --bins is not that line: it silently skips the two attestation binaries, whose required-features it does not satisfy, and says nothing about having done so. Nor is a bare tokio in the list: atproto-attestation is the only crate with a tokio feature, and Cargo refuses a bare tokio on atproto-lexicon, whose optional tokio dependency is not a feature.
# DASL operations (atproto-dasl crate)
cargo run --package atproto-dasl --features clap --bin atpcid -- '{"text":"hello"}'
cat record.json | cargo run --package atproto-dasl --features clap --bin atpcid -- --record --
echo '{"text":"hello"}' | cargo run --package atproto-dasl --features clap --bin atproto-dasl -- encode
# Identity operations (atproto-identity crate)
cargo run --features clap,hickory-dns --bin atpdid -- resolve alice.bsky.social
cargo run --features clap,hickory-dns --bin atpdid -- key generate p256
cargo run --features clap,hickory-dns --bin atpdid -- key sign signing.key data.json
cargo run --features clap,hickory-dns --bin atpdid -- key verify did:key:... data.json signature
cargo run --features clap,hickory-dns --bin atpdid -- plc verify did:plc:...
cargo run --features clap,hickory-dns --bin atpdid -- plc forks did:plc:...
# Attestation operations (atproto-attestation crate)
# `inline` takes four positionals, and the repository DID is one of them: the
# signature is bound to the repository the record will live in.
cargo run --package atproto-attestation --features clap,tokio --bin atproto-attestation-sign -- inline record.json did:plc:repo123 did:key:... metadata.json
cargo run --package atproto-attestation --features clap,tokio --bin atproto-attestation-verify -- signed_record.json did:plc:repo123
# Record operations (atproto-record crate)
cargo run --package atproto-record --features clap --bin atptid
cargo run --package atproto-record --features clap --bin atptid -- -n 5
# Repository operations (atproto-repo crate)
cargo run --package atproto-repo --features clap --bin atproto-repo-car -- ls repo.car
cargo run --package atproto-repo --features clap --bin atproto-repo-mst -- ls repo.car
# Lexicon operations (atproto-lexicon crate)
cargo run --features clap,hickory-dns --bin atproto-lexicon-resolve -- app.bsky.feed.post
# XRPC calls (atpxrpc crate)
cargo run --bin atpxrpc -- login alice.bsky.social
cargo run --bin atpxrpc -- com.atproto.repo.listRecords repo=alice.bsky.social collection=app.bsky.feed.post
ATPROTO_ACCESS_TOKEN=... ATPROTO_DPOP_KEY=did:key:... cargo run --bin atpxrpc -- dpop alice.bsky.social com.atproto.repo.listRecords repo=alice.bsky.social collection=app.bsky.feed.post
cargo run --package atpxrpc --bin atpxrpc -- --help
# Service auth tokens (atproto-oauth crate)
# Inter-service auth JWTs, `lxm`-scoped and signed by the issuer's key. This is
# service auth, not the OAuth client flow.
cargo run --package atproto-oauth --features clap --bin atproto-oauth-service-token -- --help
# Personal Data Server (atproto-pds crate)
cargo run --package atproto-pds --features clap --bin pds -- --help
cargo run --package atproto-pds --features clap --bin atproto-pds-admin -- --help
# Entryway (atproto-entryway crate): the account service in front of PDS instances
cargo run --package atproto-entryway --features clap --bin entryway -- --help
# Event streaming (atproto-jetstream crate)
cargo run --features clap --bin atproto-jetstream-consumer -- jetstream.us-east.bsky.network --collections app.bsky.feed.post --kinds commit
# TAP service consumer (atproto-tap crate)
cargo run --package atproto-tap --features clap --bin atproto-tap-client -- --help
# Rich text utilities (atproto-extras crate)
# A mention needs a DID, so --resolve-mentions (network) is what emits one;
# without it the mention is reported on stderr and left out of the array.
cargo run --package atproto-extras --features clap --bin atproto-extras-parse-facets -- --resolve-mentions "Hello @alice.bsky.social"
# MCP server (atpmcp crate)
cargo run --package atpmcp --bin atpmcp -- --help
Development #
The DASL conformance fixtures are a git submodule, so check it out before running the tests, or clone with git clone --recursive. Without it, 13 atproto-dasl tests fail and say which command to run.
git submodule update --init --recursive
CI runs these checks, among others (.tangled/workflows/ci.yml has the full list):
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
# The attestation verifier's command-line tests need two features
cargo test -p atproto-attestation --features clap,tokio
Every Rust example on this page is compiled by cargo test --workspace. Each
crate includes its own README as a doctest, and this one is included by
crates/readme-doctests, a publish = false member that exists only to hold
it. That crate's dev-dependencies are the Quick Start block, and
its tests/ check the component list, the binary list and the build line above
against cargo metadata --no-deps. An example that needs a crate the Quick
Start does not name fails to build, which is what a reader following this page
would hit.
Other useful commands:
# Build all crates
cargo build
# Generate documentation
cargo doc --workspace --open
License #
This project is licensed under the MIT License - see the LICENSE file for details.
Architecture #
These components are designed to work together as building blocks for AT Protocol applications:
- Data Foundation Layer:
atproto-daslprovides CID computation, deterministic DAG-CBOR encoding, CAR v1 archives, and pluggable block storage - Identity Layer:
atproto-identityprovides the foundation for DID resolution and cryptographic operations - Repository Layer:
atproto-repoprovides Merkle Search Tree operations, commit structures, and repository handling - Data Layer:
atproto-recordprovides the record wire types and the community lexicon types, andatproto-attestationcryptographic attestations; a record's CID comes fromatproto-dasl(compute_cid_from_json) - Schema Layer:
atproto-lexiconvalidates records and XRPC calls against lexicon schemas, and resolves those schemas by NSID - Authentication Layer:
atproto-oauthhandles OAuth 2.0 flows with AT Protocol security extensions, andatproto-xrpcsthe service-auth tokens services present to each other - Application Layer:
atproto-clientandatpxrpcenable client applications and service development - Server Layer:
atproto-pdsis a Personal Data Server built from all of the above, andatproto-spaceandatproto-space-clientare its permissioned-data half - Event Layer:
atproto-firehose,atproto-jetstreamandatproto-tapprovide real-time event processing capabilities - Utilities Layer:
atproto-extrasprovides rich text facet parsing for AT Protocol applications
Use Cases #
This workspace enables development of:
- Personal Data Servers -
atproto-pdsis one, and is also a worked example of assembling the rest - OAuth Clients and Authorization Servers - Confidential clients through
atproto-oauth's workflow, and the DPoP, PKCE and JWT pieces an authorization server validates with - AT Protocol Clients - Applications that authenticate and interact with AT Protocol services
- Inter-service APIs - XRPC services that authenticate their callers with policy-checked service-auth tokens
- Event Processing Systems - Real-time processors for AT Protocol repository events via the firehose, Jetstream or Tap
- Repository Tools - CAR file inspection, MST analysis, and repository verification utilities
- Development Tools - CLI utilities for testing, debugging, and managing AT Protocol identities
Contributing #
Contributions are welcome! This project follows standard Rust development practices:
- Fork this repository
- Create a feature branch
- Run tests:
git submodule update --init --recursiveonce, thencargo test --workspace(see Development) - Run linting:
cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings - Submit a pull request
Acknowledgments #
Parts of this project were extracted from the smokesignal.events project, an open-source AT Protocol event and RSVP management application, now a read-only archive. This extraction enables broader community use and contribution to AT Protocol tooling in Rust.