We don't have to live like this
I got invited to give a talk at AtmosphereConf 2026. It was really awesome to fly out to Vancouver and meet all of the wonderful and passionate people I know from the atproto dev community on Bluesky (and other cool people I didn't know) and just to be in a space with so many like-minded peopleThe conference was incredibly diverse, lots of women, queer and trans people, non-white people, activists, scientists, journalists, and generally the kind of person who often feels unwelcome at the average tech conference. It was honestly incredible., but I'll talk about it in more detail elsewhere. Unfortunately I contracted a fairly serious respiratory illness (not COVID like some others, best guess was RSV) either at the conference or on the trip home, and so I didn't get round to writing anything up until now. I titled my talk the same as the original Jacquard blog post. It's a pretty good talk, I'm told, you should go watch it (though it's not a prerequisite for reading this). The website I'm sending you to for the video archive of the talk is my entry into the Streamplace VOD Jam (because stream.place shipped support for videos on demand during the conference without a UI for them, and Eli tasked the community with coming up with fun and useful ways to allow people to watch the recorded talks), which is of course built with JacquardEmbed support for those in Weaver, the platform you're reading this on, is still to come, along with video embeds in general (but the stuff built for vod.place will come in handy there). I got nerdsniped and basically rewrote a badly vibe-coded pure Rust implementation of the ASS subtitle format, favoured by anime fans for its incredibly powerful capabilities for styling text and generally rendering stuff on top of a video, as well as making major improvements to a Rust HLS playlist crate and making a Dioxus-based video player on top of all that.. I wanted to keep it accessible, so I didn't do as much of a deep dive as I perhaps could have and kept more to the motivations and broader philosophy, as well as showing off a cool demo, which uses the bevy game engine and Jacquard to visualize the atproto firehose (well, Jetstream, the 'lite' version, because it needed to work on conference WiFi) as a multi-layered, rewindable time helix, including loading and displaying (using the same sorts of tricks Weaver uses for record display) almost every event on the network on request, using Constellation and Slingshot. Jacquard, particularly the new beta version I was showing off, makes this remarkably easy, and that is of course the point.
What follows is the deep dive the conference talk didn't include
The demo shows a number of the ways you can use Jacquard, including how easy it is to plug-and-play your own implementation of a core trait (in this case the WebSocketClient trait) to make things work in a different async runtime. The surf_xrpc_channel example from the demo repository shows both that and a quick-and-dirty blocking/polling implementation of something very much like Jacquard's own XRPC traits.
If you go read the equivalent bits of Jacquard, and then read the above, you can see how much of this simply reuses the existing infrastructure, including the request builder and output parsing, such that the example is almost entirely generic glue to connect Bevy's blocking runtime context with an async http client (in this case surf) in a way that fits Jacquard patterns and is driven by the same trait implementations it already generates.
Ease of Use
A friend told me "...I have less than 10 hours of rust experience total and had no problem building a useful tool with what you wrote". That friend is an extremely skilled C++ developer, so he is perhaps not the best person to evaluate ease of use overall, but he's also not the first to say very similar thingsI should note that I have a bit of a difficult time talking about this. There were a lot of little decisions I made when writing Jacquard that play into its ease of use, and many were intuitive ones, based on what felt right to me, what I wanted out of an atproto library, and how to make one that got out of my way and let me focus on the stuff I actually wanted to be doing. This whole essay is basically me trying to explain my intuitions.. Jacquard is incredibly easy to use, so what makes it easy?
Managing Complexity
atproto is complex. This isn't really the fault of the people who founded Bluesky, much of it is somewhat inherent to the problems they were trying to solve with the protocol (even the superficially much more straightforward ActivityPub ends up with a lot of complexity under the hood, both explicit and implicit). Decentralized identity, a language for specifying interoperable interfaces (Lexicon), the split between where user data lives (the PDS), the authority that governs user identity (plc.directory for did:plc or your own domain registrar for did:web), what indexes it and pushes out the global event stream (the Relay), and what consumes that data, indexes it, and provides the interfaces people use (appviews, indexes, and apps). However, that complexity can make it intimidating start building on. There's a lot to think about, if you're starting from scratch. And of course sometimes you do need to think about it. But you shouldn't have to right from the get-goSomeone described the thing that makes Jacquard easy to use as "hiding complexity." I don't think that's accurate. Jacquard will happily show you all the complex guts, you can get way down in there if you want or need to. What it doesn't do is force you to deal with it all up front. It gates it, does progressive disclosure. It's designed to help you along the way, and provide machinery that does the tedious stuff.. If I had to put down in one sentence the fundamental principle of Jacquard's design, from the perspective of the developer looking to use the library, it would be the following.
You should only have to care about the complexities that matter for what you want to do now
If you are doing atproto OAuth, you will need to think about what scopes you want to use eventually, and how you need to host your client metadata, and how you need to handle user sessions, eventually. But for your quick-and-dirty prototype? You should be able to have something that lets you authenticate a user to do things on atproto in as few lines of code as possible, without having to understand all that right up front. That's why Jacquard has the local loopback server feature. And other good atproto auth libraries, like @fujocoded/authproto, which integrates with the Astro framework, do similar things.
Good defaults to start with
Jacquard aims to provide immediately useful and straightforward defaults for everything. Some things (like the various storage traits) you should likely write your own implementations of if you want to use Jacquard in production (Jacquard does not provide implementations of them targeting any database libraries, for example, because any such implementation would inevitably need adapting for your own app's schema, and providing a default that's easy to use but performs badly would hurt more than it helps), but they're designed to be pluggable and have a simple default you can start withThe latest beta version was all about improving that default experience without sacrificing the "power user" experience. Jacquard, for its simplicity, had one major hangup in terms of approachability, heavy use of lifetimes, a concept only the Rust language really foregrounds, which a lot of people find confusing. Of course I did my best to make it all work well, but a few pain points remained, like how no Jacquard generated API type could implement the serde Deserialize trait for any and all lifetimes (expressed as for<'de> Deserialize<'de> or DeserializeOwned) due to that inherent lifetime on almost every base string type.. A second principle is something like
Never make someone reimplement the wheel just to colour outside the lines
This was by far the biggest pain point of the existing atproto libraries I was familiar with when I started working on Jacquard. They might provide good defaults, but the moment you needed to do something they didn't expect (and that could be something as simple as using stuff that wasn't in the com.atproto or app.bsky lexicon namespaces), you were stuck reimplementing what felt like half the scaffolding in the library in order to make that happen.
That is why Jacquard leans so hard on making it easy to work with essentially the raw generated API types, even down to making the generated source files read mostly like something a person might have written, so if you do need to go poking around in there, it's easy to understand. After all the most common thing you are going to do making an atproto app that's not a Bluesky client is write and use your own custom lexicons. That therefore needs to be as low-friction as possible.
Extensions should feel like part of the core experience
My goal is that working with those types should feel identical to working with the types in Jacquard's included API crate (because both are just code that's been generated the exact same way). You shouldn't need to write extra wrappers to make single API calls straightforward. But if you do need to do stuff on top of what Jacquard already provides, you should be able to access those wrappers on Jacquard's own built-in types, or things that implement the traits those types doJacquard makes substantial use of 'extension traits', where a trait has all provided methods, and is automatically implemented and provides those methods for any type that implements the prerequisite traits. If you make an extension trait that takes AgentSessionExt as a super-trait, you can use your custom helpers on literally any struct that implements the prerequisites for that trait, which is any valid XrpcClient + IdentityResolver implementation. Both of those traits take a little more doing to implement out of necessity, especially if you're implementing them from scratch, rather than wrapping an OAuth or app password session implementation, but you get a lot from them., if you write them in a way that fits Jacquard's philosophy. All the traits and types interlock under the hood to make this coherent system that lets simple generated structs that implement straightforward traits (which primarily provide information) drive all this protocol machinery.
There are times when that doesn't cut it. For example, if you need more control over the caching in the default identity resolver, you're on the hook for a lot of work to reimplement it. That's something I actually want to fix somewhat, but it's also why Jacquard is a series of crates that you can pick and choose bits from, where only jacquard-common without its default features is really critical, and where you can override essentially every trait quite easily, and re-use helper functions to make that easierThis is what I mean when I say that Jacquard doesn't actually hide the complexity. It actually has a remarkably large API surface compared to a lot of superficially similar Rust libraries. There's a lot of "internals" available, and that's deliberate. A common frustration of mine is actually with how, possibly in service of making maintaining sem-ver compatibility easier, a lot of libraries expose very limited public API surfaces. It makes them simpler on the surface, but at the cost of potentially needing to fork the library if you need access to something they don't deign to make public. The risk of Jacquard's approach is the UX problem a lot of open-source software has. The programmer isn't confident enough to make a decision on which way to do something, so they make it a configuration option. But defaults matter a ton, which is why I put a lot of thought into the defaults for Jacquard, while still offering all the "configuration options", for those who need them, just not right front-and-centre.
The answer to OSS UI disease isn't to be GNOME (or Apple) and say "it's our way or the highway". It's to be very deliberate about your defaults, but still empower people. A lot of what atproto does right as a protocol for democratizing the social web is the ways in which it facilitates this at an app, data, and network level.. Jacquard's default implementation of its WebSocket client trait doesn't auto-reconnect! This is deliberate, because what you want to do on a connection drop is inherently dependent on your app's needs, and is hard to make work for a wide variety of use cases without producing opaque failure modes or becoming complex to configureThis desire for pluggability and flexibility is also why Jacquard has both Rust->lexicon and lexicon->Rust paths to generated implementations of the core API traits. Whether you start with lexicon JSON or a Rust struct, you should be able to .send() it, if it's an XRPC call..
Smart type-driven design
The ability to just .send() it comes down to how Jacquard's types are designed. For those not familiar with Rust, Rust has 'traits' which are sort of like 'interfaces' in some other languages. A trait in Rust describes a set of behaviours. It can also have associated types and constants. Here is the XrpcRequest trait, implemented for every API request parameters struct.
The trait has two constants, an associated type, and two provided methods (which can be overridden). The response type is assumed to be a marker structA zero-sized struct that exists to represent a conceptual thing that doesn't take up space at runtime, in this case the type of the response that should be expected for a given request. implementing a related trait that handles the response side similarly. The constants and type provide information that the machinery that accepts structs that implement this trait uses to determine what endpoint on the host to make the request to and what HTTP method (GET POSTNote that XRPC queries (HTTP GET requests) encode the struct as query parameters in the URL. The builder function handles this properly. or ) to use on it. It also provides the encode call for the client and the decode call for the server. Those are defaults, and can of course be overridden for binary bodies, CBOR bodies, or in the case of vod.place, HLS playlist encodingThis involved manually implementing the trait in question, as the code generation and derive macros have limited support for other encodings (binary bodies are the only non-JSON case the code generation handles automtically for normal XRPC requests, XRPC subscriptions also handle CBOR).
/// Response type for place.stream.playback.getVideoPlaylist
;
The result is a state machine driven by the type systemThis is pretty well-tread territory if you're familiar with functional programming, or have read a lot about certain kinds of Rust development (e.g. embedded Rust).. The compiler, based on the resolved types at the call sites, is able to determine what code needs to run, what the target URL contains, what codec functions are called, and so on, what 'success' and 'failure' outputs look like, without the kinds of overhead imposed by reflection or virtual function tablesThere is overhead, but it's primarily in the size of the resulting binary due to monomorphization, and this can be managed somewhat. A dyn-compatible version of the XRPC call traits, potentially one that makes use of the facet crate, is in the works, in part because platforms like WebAssembly care a good bit more about binary size than absolute speed in many cases, so being able to avoid monomorphization bloat is valuable, when your primary constraint is the network..
"Parse, don't validate" considered harmful
...sometimes. There are subtleties to how you use types to drive things, and it's important to know when not to have the type system dictate somethingSomething discussed in my talk at some length.
That is why Jacquard's handle constructor has a specific carveout for handle.invalid
Because there's a time to follow the spec
And there's a time to refrain"Parse, don't validate" is good advice, and Jacquard is all about type-driven design, but you should never take even the best advice as gospel. And it is precisely those scenarios which you need to consider when you're writing a library that needs to not only implement an extremely flexible protocol, but also interoperate with other implementations of that protocol which may or may not be quite as anal as Rust about types. That leads into another principle, and one that I think Jacquard could do a lot better with, even if it's already quite good.
Fallbacks and failure states matter a ton. Make them reasonable to work with.
Dealing with untrusted data from over a network, everything is fallible. There are so many layers of abstraction and hardware between a server pushing out some JSON in response to your requests and serde_json trying to parse out your desired type from the response body it's almost comical. This is both obviously true and requires nontrivial design work. Rust's de facto default way to "handle" an error is the ? operator, which basically just punts responsibility to the caller. This is often correct, in the common case where you can't really recover in the local scope, but how do you surface the errors in a useful way that doesn't make them basically just exceptions? Rust has a lot of nice stuff around the Result type, and a number of Rust crates (Jacquard uses thiserror and mietteYou run miette? You run her code like the software? Oh. Oh! Error code for coder! Error code for One Thousand Lines!
Rust developers are Just Like That. We also have cargo-mommy. There's a time to be serious, and a time to have a bit of fun with it.) seek to make good and useful and nice to read errors easy to produce. But that's only half of the problem. One distinction Jacquard tries to make is between transport errors (HTTP errors, or invalid authentication, etc.) and protocol errors (i.e. the XRPC endpoint returned a specific error). There's some overlap (e.g. serialization/deserialization errors can happen in both, technically, and some valid protocol errors that might not get caught at the transport stage are auth-related) but in general the division holdsThere are some mechanical reasons for the separation as well. Because of the need to borrow the deserialized output from the response buffer in some configurations, and because of the desire to handle refreshes and such for authentication internally in the XrpcClient implementations for the different session types, without exposing them as errors that need handling by the caller under most circumstances, there needed to be this clear delineation between stuff that required parsing or consuming the response and stuff that didn't..
Partial Understanding
The part of this I haven't solved yet, despite significant effort, is the "your response almost but not quite parsed as the type you asked for" scenario. What good is a partial understanding?, from the Self-Directed Research podcast by James and Amos (aka @fasterthanli.me) made massive inroads in my brain, and making Jacquard as good as possible at working with data that's imperfect in a way that's just as straightforward as working with data that's perfect is a major long-term goal. atproto data itself, particularly when it comes to records and unions is more akin to a 'self-identifying format' (from the episode) than a self-describing one, though obviously it is implemented on top of two self-describing serialization formats (JSON and CBOR). If you don't know what you're dealing with, you can resolve the lexicon from the NSID and figure it out. The question is of course, what thenEveryone encountering the standard.site document content schema other than me seems to have this question.? How can you do useful things on data you just figured out how to parse this moment at runtime? Well there's some obvious answers to specific cases, and where you develop a secondary schema to describe your schema that describes your schema, including stuff like page.parts from the Leaflet guys, or inlay.at from Dan Abramov (conference talk link). There's stuff like panproto, and DASL and RASLRobin Berjon, you have amazing taste in acronyms.. But what those mostly solve for is the problem of "I have valid data of a type I'm not familiar with, tell me what to do with it, please?" Which is a fine enough problem to solve in an open ecosystem, but also, hashtag skill issueWeaver's own generic record handling isn't perfect but it does work quite consistently across all sorts of atproto records, while keeping a consistent style, and doesn't need a separate "display format resolution" step, and it does this in part because of the power of Data<S>.. Jacquard currently has the Data<S> type for freeform (mostly) valid atproto data, and most lexicon types have a flattened extra_data field that holds a map of Data<S> values. But this is obviously imperfect. It handles unknown fields, but not invalid ones presently (that would require custom deserialization implementations it currently doesn't have). It's more of an upgrade to the serde_json::Value type than a true answer to this question, in spite of all the affordances (like JSONPath-style querying) it has built into it. The harder problem is, as I said, invalid but mostly well-formed data. Data that has an invalid item for one field of one item in an array (the canonical example is a Bluesky feed response, an array of FeedViewPost, where one person has an invalid atproto handle). The protobuf solution is to make every field optional. That is an understandable tradeoff, particularly given some of the internal headaches described as being caused by intermediate layers having to validate if required fields were present or not, but it is not one that Paul Frazee made with Lexicon, and it's not one I like personally. To me it's a cop-out.
We don't have to live like this!
A lot of the motivation for Jacquard's design and existence boils down to that refrain. I got frustrated with bad code generation that made you do a ton of work to integrate its output and make it usable. I got frustrated with libraries that weren't designed with care, to present a good way of interacting with whatever they were supposed to help you interact with while allowing freedom to mix-and-match, with APIs that returned cryptic stack traces for error messages that required reading the original source code at length to understand, and in general, with a seeming lack of effort and care, without understanding the developer as a type of end-user themselves. The best Rust librariesThis is obviously true of other languages' good libraries, as well, but I have found the Rust ecosystem to be far better than average here, particularly compared to other compiled languages. are great teachers, not just powerful and ideally intuitive tools. They, like the Rust language itself, guide you toward writing better code, and into good patternsThis is one of the biggest strengths of the language, in my opinion. The borrow checker, lifetimes, and so on all teach you, via really good compile errors, how to write code that is memory-safe and generally decent, automatically, implicitly. You develop really good muscle memory for it over time, but you still (barring unsafe code) always have the check there in case you forget.. Jacquard has a small tutorial about lifetimes and deserialization in the jacquard lib.rsjacquard-common lib.rsNote that this is out-of-date, the latest beta makes this less critical, with the swap to the borrow-or-share pattern over mandatory lifetimes., and a longer one adapted from the initial blog post in the as well. There's a lot that I'd like to improve about the documentation and a number of things I think need reorganization, still, but I think that will always be true. But even so, I think the library does a pretty good job in and of itself of teaching you, gradually, how it works and how atproto works, without overwhelming you. The decision to basically rethink the fundamental primitive behind the entire library in order to make it easier to work with going forward is perhaps indicativeI (and Claude) rewrote the entire library in a weekend, the week before the conference.. I was very conscious of the fact that I was breaking the code of everyone depending on the library, which is why I've done an extended beta of it, so that people could test it out. But once I began the refactor and did some early tests to validate that the core idea was sound, I was almost immediately convinced that this was definitely the right path forward for Jacquard.
Ultimately, I saw that I could meet a need for myself and some developer friends, and decided to lead by example, to demonstrate conclusively that things could in fact be done a lot better in many ways, if we cared to, and to try and raise the quality waterline overall in the atproto developer ecosystem, and perhaps more broadly.
Future
Jacquard isn't done. It's both not feature-complete (it needs to support a few more atproto functions for me to be comfortable saying that) and until I solve the partial-understanding problem to my satisfaction, without making compromises I dislike, I won't be satisfied with it. But it's very good, and very easy to use, and I think even if you don't want to develop for the AT Protocol in Rust, there's something to learn from how it's madeLLMs have made code cheap. Don't be satisfied with half-solutions, or suboptimal trade-offs. Don't make slop more efficiently, there was already more than enough bad code in the world. There's not always a third option, but increasingly, a person who cares deeply about doing things right can do that to a level that was never feasible before.. I hope this was interesting. I know at least one person was curious about my thought process designing Jacquard, and hopefully this provides some illumination there, without being too denseThat's always the tendency I'm fighting when I write. How do I convey everything needed to make my point or allow people to understand what I'm talking about without rationalist-level verbosity, or being far too opaque.. It was also, I think, good for me, to attempt to put into words the way I think about these things.