···11+---
22+name: working-with-jacquard
33+description: Use when working with Jacquard AT Protocol library for Rust - prevents type parameterization mistakes, string allocation antipatterns, and incorrect BosStr usage patterns
44+---
55+66+# Working with Jacquard
77+88+## Overview
99+1010+Jacquard is a Rust AT Protocol library that uses a borrow-or-share type system for correctness, performance, and ergonomics. All API types are parameterized on `S: BosStr = DefaultStr` — the caller chooses the backing string type, not the library.
1111+1212+**Core principle:** Borrow wherever possible, own when needed. The `DefaultStr` (`SmolStr`) default makes ownership painless, but good code still prefers borrowed deserialization, borrowed function parameters, and borrowed intermediates. Validate at construction time.
1313+1414+**Announce at start:** "I'm using the working-with-jacquard skill to ensure correct BosStr and type usage."
1515+1616+## The BosStr Type System
1717+1818+All API types use a single generic parameter `S: BosStr` instead of lifetimes:
1919+2020+```rust
2121+// Generated API type (e.g. Post)
2222+pub struct Post<S: BosStr = DefaultStr> {
2323+ pub text: S,
2424+ pub created_at: Datetime,
2525+ pub embed: Option<PostEmbed<S>>,
2626+ // ...
2727+}
2828+```
2929+3030+**BosStr implementors** (caller's choice):
3131+3232+| Type | Allocates? | `DeserializeOwned`? | Use when |
3333+|------|-----------|---------------------|----------|
3434+| `&str` | No | No | **Preferred.** Function params, ephemeral/internal values |
3535+| `CowStr<'a>` | Only if needed | No | **Preferred for deser.** Zero-copy from buffers |
3636+| `SmolStr` (= `DefaultStr`) | Inline <=23 bytes, Arc longer | Yes | When data must outlive its borrow source |
3737+| `String` | Yes | Yes | Interop with String-based APIs |
3838+3939+**Key insight:** The `SmolStr` default makes ownership ergonomic — no lifetime ceremony, `DeserializeOwned`, `'static` — but it's a convenience floor, not a ceiling. Prefer borrowed types (`&str`, `CowStr<'_>`) wherever possible and only reach for owned when the data must outlive its source.
4040+4141+4242+## Critical Patterns
4343+4444+### Client traits in scope
4545+4646+Nearly all methods for making API calls are provided via traits, which must be in scope.
4747+There are many extension traits that are auto-implemented for **any** struct meeting the prerequisites.
4848+4949+The most common pair is `IdentityResolver + AgentSession`, which gives access to `AgentSessionExt` with record CRUD helpers.
5050+5151+`use jacquard::prelude::*;` brings the critical ones into scope.
5252+5353+**Rule:** Always import the prelude. If the compiler says a method doesn't exist, you likely need a trait in scope. READ the error output carefully.
5454+5555+### String Type Constructors
5656+5757+All validated types (`Did`, `Handle`, `AtUri`, `Nsid`, `Tid`, `Cid`, etc.) are parameterized on `S: BosStr = DefaultStr`:
5858+5959+```rust
6060+// PREFERRED: SmolStr-backed (inline, no heap for short strings)
6161+let did = Did::new("did:plc:abc123")?;
6262+6363+// BEST: Zero-allocation for string literals
6464+let nsid = Nsid::new_static("com.atproto.repo.getRecord");
6565+6666+// When you already have an owned String
6767+let owned = Did::new_owned(some_string)?;
6868+6969+// AVOID: FromStr always allocates a fresh SmolStr
7070+let did: Did = "did:plc:abc123".parse()?;
7171+```
7272+7373+**Borrowing and conversion:**
7474+7575+```rust
7676+// Cheap borrow — returns Type<&str>
7777+let did_ref: Did<&str> = did.borrow();
7878+7979+// Cross-type conversion
8080+let did_string: Did<String> = did.convert::<String>();
8181+```
8282+8383+**Rule:** Use `new()` for default, `new_static()` for string literals, `new_owned()` when you have a `String`. Avoid `FromStr::parse()` unless performance is irrelevant.
8484+8585+### Function Parameters: Borrow by Default
8686+8787+Function parameters should almost always accept borrowed types. There are three levels of borrowing and you should use the cheapest one that works:
8888+8989+```rust
9090+// BEST: Reference to default-backed type (most flexible for callers)
9191+fn process_did(did: &Did) { /* ... */ }
9292+9393+// GOOD: Borrow-parameterized type (zero-copy inner string)
9494+fn process_did(did: Did<&str>) { /* ... */ }
9595+9696+// MOST BORROWED: Both (reference to borrow-parameterized type)
9797+fn process_did(did: &Did<&str>) { /* ... */ }
9898+9999+// AVOID: Taking ownership when you don't need it
100100+fn process_did(did: Did) { /* ... */ } // consumes unnecessarily
101101+```
102102+103103+For compound types, same principle — borrow the outer type and/or parameterize with a borrowed backing:
104104+105105+```rust
106106+// GOOD: Borrow the struct, strings are still SmolStr inside
107107+fn process_post(post: &Post) { /* ... */ }
108108+109109+// BETTER: Zero-copy all the way down
110110+fn process_post(post: &Post<CowStr<'_>>) { /* ... */ }
111111+112112+// Generic: Accept any backing type
113113+fn process_post<S: BosStr>(post: &Post<S>) { /* ... */ }
114114+```
115115+116116+**Rule:** Don't take ownership of types unless you need to store or return them. Use `&Type`, `Type<&str>`, or `&Type<&str>` for function parameters.
117117+118118+### Response Parsing: Prefer Borrowed
119119+120120+XRPC responses wrap a `Bytes` buffer. Caller chooses backing type at parse time:
121121+122122+```rust
123123+let response = agent.send(request).await?;
124124+125125+// PREFERRED: Zero-copy borrow from buffer
126126+let output = response.parse::<CowStr<'_>>()?;
127127+// output borrows from response — both must stay in scope
128128+// use this when processing in the same scope (the common case)
129129+130130+// When the borrow can't live long enough:
131131+let output = response.into_output()?;
132132+// output is SmolStr-backed, 'static, DeserializeOwned
133133+134134+// Untyped (when you don't know the schema):
135135+let data = response.parse_data()?; // Data<CowStr<'_>>
136136+let raw = response.parse_raw()?; // RawData<'_>
137137+```
138138+139139+**Rule:** Prefer `.parse::<CowStr<'_>>()` and only fall back to `.into_output()` when the compiler tells you the borrow doesn't live long enough. Borrowed types *can* cross async boundaries depending on scope and lifetime inference — don't assume async means owned.
140140+141141+**When you genuinely need `DeserializeOwned`:** Some frameworks require it structurally — axum extractors (e.g. `Json<T>`) call `serde_json::from_reader` internally which requires `DeserializeOwned`, and dioxus `use_server_future()` closure arguments have the same constraint. In these cases, use `SmolStr`-backed types (the default). These are the situations the default exists for.
142142+143143+### IntoStatic Trait
144144+145145+Converts any `BosStr`-backed type to its `SmolStr` (owned, `'static`) equivalent:
146146+147147+```rust
148148+use jacquard::common::IntoStatic;
149149+150150+// Convert a CowStr-backed (borrowed) type to owned
151151+let borrowed = response.parse::<CowStr<'_>>()?;
152152+let owned: Post<SmolStr> = borrowed.into_static();
153153+154154+// Also works for validated string types
155155+let did_ref: Did<&str> = did.borrow();
156156+let did_owned: Did<SmolStr> = did_ref.into_static();
157157+```
158158+159159+**When you need it:**
160160+- Converting borrowed (`CowStr<'_>` or `&str`) types to owned for storage or return
161161+- Custom types that use non-default backing strings
162162+163163+**When you don't need it:**
164164+- `SmolStr`-backed types are already `'static` — no conversion needed
165165+- Using `.into_output()` already gives you `SmolStr`-backed types
166166+167167+**Rule:** All custom types with `S: BosStr` parameter MUST derive `IntoStatic`. But for typical usage with default `SmolStr` backing, you rarely call it directly.
168168+169169+### Data vs serde_json::Value
170170+171171+**Never use `serde_json::Value` with Jacquard.**
172172+173173+```rust
174174+// NEVER
175175+let value: serde_json::Value = serde_json::from_slice(bytes)?;
176176+177177+// ALWAYS use Data<S> (owned) or RawData<'a> (zero-copy)
178178+use jacquard::common::{Data, from_data, to_data};
179179+180180+let data: Data = serde_json::from_slice(bytes)?; // Data<SmolStr>
181181+let post: Post = from_data(&data)?;
182182+183183+// Convert typed -> untyped -> typed
184184+let post = Post::builder().text("test").build();
185185+let data: Data = to_data(&post)?;
186186+let post2: Post = from_data(&data)?;
187187+```
188188+189189+**Two value types:**
190190+- `Data<S: BosStr>`: Typed, owned by default. Use for storage, manipulation, serialization.
191191+- `RawData<'a>`: Lifetime-based, zero-copy. Use for transient parsing from buffers.
192192+193193+**Path access on Data:**
194194+```rust
195195+let data: Data = /* ... */;
196196+197197+// Path syntax: field.nested, [0] for arrays
198198+if let Some(alt) = data.get_at_path("embed.images[0].alt") {
199199+ println!("{}", alt.as_str().unwrap());
200200+}
201201+202202+// Query syntax: [..] wildcard, ..field scoped recursion, ...field global recursion
203203+let alts = data.query("embed.[..].alt");
204204+let handle = data.query("post..handle"); // finds post.author.handle
205205+let all_cids = data.query("...cid"); // all CIDs anywhere
206206+```
207207+208208+Path access has `_mut` and `set_at` equivalents for mutation.
209209+210210+**Rule:** `Data<S>` is Jacquard's replacement for `serde_json::Value`. Always use it for untyped AT Protocol values.
211211+212212+213213+## Common Mistakes
214214+215215+### Using Lifetimes Instead of BosStr on Custom Types
216216+217217+```rust
218218+// WRONG: Old lifetime-based pattern
219219+struct MyOutput<'a> {
220220+ #[serde(borrow)]
221221+ field: CowStr<'a>,
222222+}
223223+224224+// CORRECT: BosStr-parameterized
225225+#[derive(Serialize, Deserialize, IntoStatic)]
226226+#[serde(bound(deserialize = "S: Deserialize<'de> + BosStr"))]
227227+struct MyOutput<S: BosStr = DefaultStr> {
228228+ field: S,
229229+ #[serde(flatten, default, skip_serializing_if = "Option::is_none")]
230230+ extra_data: Option<BTreeMap<SmolStr, Data<S>>>,
231231+}
232232+```
233233+234234+**Why:** Jacquard types no longer use lifetime parameters. Using `<'a>` instead of `<S: BosStr>` means your type can't compose with Jacquard's API types or response parsing.
235235+236236+### Forgetting the Serde Bound
237237+238238+```rust
239239+// WRONG: Missing serde bound — deserialization will fail
240240+#[derive(Serialize, Deserialize)]
241241+struct MyType<S: BosStr = DefaultStr> {
242242+ name: S,
243243+}
244244+245245+// CORRECT: Explicit serde bound for BosStr
246246+#[derive(Serialize, Deserialize)]
247247+#[serde(bound(deserialize = "S: Deserialize<'de> + BosStr"))]
248248+struct MyType<S: BosStr = DefaultStr> {
249249+ name: S,
250250+}
251251+```
252252+253253+**Why:** Serde can't infer the correct bounds for generic types. Without the explicit bound, the derive generates `S: Deserialize<'de>` which is insufficient.
254254+255255+### Roundtripping Through String
256256+257257+```rust
258258+// BAD: Pointless allocation
259259+let did_str = did.as_str().to_string();
260260+let did2 = Did::new(&did_str)?;
261261+262262+// GOOD: Clone, borrow, or convert
263263+let did2 = did.clone();
264264+let did_ref = did.borrow(); // Did<&str>, zero-cost
265265+let did_string = did.convert::<String>(); // cross-type
266266+```
267267+268268+### Dropping Response While Holding Borrowed Parse
269269+270270+```rust
271271+// WILL NOT COMPILE
272272+let output = {
273273+ let response = agent.send(request).await?;
274274+ response.parse::<CowStr<'_>>()? // borrows from response
275275+}; // response dropped!
276276+277277+// CORRECT: Keep response alive
278278+let response = agent.send(request).await?;
279279+let output = response.parse::<CowStr<'_>>()?;
280280+281281+// OR: Use into_output() for owned types (usually better)
282282+let output = agent.send(request).await?.into_output()?;
283283+```
284284+285285+### Calling .into_static() When You Don't Need To
286286+287287+```rust
288288+// WASTEFUL: into_output() already gives SmolStr-backed types
289289+let response = agent.send(request).await?;
290290+let output = response.into_output()?;
291291+let owned = output.into_static(); // redundant! already SmolStr
292292+293293+// CORRECT: into_output() is sufficient
294294+let output = agent.send(request).await?.into_output()?;
295295+// output is already SmolStr-backed and 'static
296296+```
297297+298298+### Not Deriving IntoStatic on Custom Types
299299+300300+```rust
301301+// WILL NOT COMPILE with zero-copy parsing
302302+#[derive(Serialize, Deserialize)]
303303+#[serde(bound(deserialize = "S: Deserialize<'de> + BosStr"))]
304304+struct MyOutput<S: BosStr = DefaultStr> {
305305+ field: S,
306306+}
307307+308308+// REQUIRED: Derive IntoStatic
309309+#[derive(Serialize, Deserialize, IntoStatic)]
310310+#[serde(bound(deserialize = "S: Deserialize<'de> + BosStr"))]
311311+struct MyOutput<S: BosStr = DefaultStr> {
312312+ field: S,
313313+}
314314+```
315315+316316+### Forgetting MST Immutability
317317+318318+MST (Merkle Search Tree) is immutable and persistent:
319319+320320+```rust
321321+// WRONG: Loses result
322322+mst.add(key, cid).await?;
323323+324324+// CORRECT: Reassign
325325+let mst = mst.add(key, cid).await?;
326326+```
327327+328328+### Skipping Issuer Verification in OAuth
329329+330330+```rust
331331+// SECURITY VULNERABILITY
332332+let token_response = exchange_code(...).await?;
333333+// Immediately trusting token_response.sub without verification!
334334+335335+// ALWAYS VERIFY
336336+let token_response = exchange_code(...).await?;
337337+let pds = resolver.verify_issuer(&server_metadata, &token_response.sub).await?;
338338+```
339339+340340+### Non-Exhaustive Union Matches
341341+342342+Open unions have `Unknown(Data<S>)` variant:
343343+344344+```rust
345345+// WILL NOT COMPILE (missing Unknown variant)
346346+match embed {
347347+ PostEmbed::Images(img) => { /* ... */ }
348348+ PostEmbed::Video(vid) => { /* ... */ }
349349+}
350350+351351+// HANDLE ALL VARIANTS
352352+match embed {
353353+ PostEmbed::Images(img) => { /* ... */ }
354354+ PostEmbed::Video(vid) => { /* ... */ }
355355+ _ => { /* Unknown or other variants */ }
356356+}
357357+```
358358+359359+### Forgetting extra_data Field
360360+361361+When constructing types without builders:
362362+363363+```rust
364364+// WRONG: Missing extra_data
365365+let record = MyRecord {
366366+ known_field: value,
367367+};
368368+369369+// CORRECT: Include extra_data
370370+let record = MyRecord {
371371+ known_field: value,
372372+ extra_data: None,
373373+};
374374+375375+// BETTER: Use builder (handles it automatically)
376376+let record = MyRecord::builder()
377377+ .known_field(value)
378378+ .build();
379379+```
380380+381381+382382+## Quick Reference
383383+384384+### String Type Constructors
385385+386386+| Method | Allocates? | Use When |
387387+|--------|-----------|----------|
388388+| `new(s)` | SmolStr (inline <=23b) | Default construction |
389389+| `new_static(&'static str)` | No | String literals |
390390+| `new_owned(String)` | Reuses buffer | Already have a String |
391391+| `.borrow()` | No | Cheap `Type<&str>` reference |
392392+| `.convert::<B>()` | Depends on B | Cross-type conversion |
393393+| `FromStr::parse()` | **Always** | **Avoid** |
394394+395395+### Response Parsing
396396+397397+| Method | Backing | Allocates? | Use When |
398398+|--------|---------|-----------|----------|
399399+| `.parse::<CowStr<'_>>()` | `CowStr<'_>` | No (zero-copy) | **Preferred.** Processing in same scope |
400400+| `.parse_data()` | `Data<CowStr<'_>>` | No | Untyped, zero-copy |
401401+| `.parse_raw()` | `RawData<'_>` | No | Raw untyped, zero-copy |
402402+| `.into_output()` | `SmolStr` | Yes (inline) | When borrow can't live long enough |
403403+404404+### BosStr Backing Types
405405+406406+| Type | Owned? | `DeserializeOwned`? | Typical Use |
407407+|------|--------|---------------------|-------------|
408408+| `SmolStr` (default) | Yes | Yes | When borrow can't live long enough |
409409+| `&str` | No | No | Function params, ephemeral |
410410+| `CowStr<'a>` | Borrow-or-own | No | Zero-copy from buffers |
411411+| `String` | Yes | Yes | Interop |
412412+413413+### Data Types
414414+415415+| Use | Don't Use |
416416+|-----|-----------|
417417+| `Data<S>` (owned) | `serde_json::Value` |
418418+| `RawData<'a>` (zero-copy) | `serde_json::Value` |
419419+| `from_data()` | `serde_json::from_value()` |
420420+| `to_data()` | `serde_json::to_value()` |
421421+422422+423423+## Red Flags
424424+425425+**CATASTROPHIC (stop immediately):**
426426+- Using `serde_json::Value` instead of `Data`
427427+- Skipping OAuth issuer verification
428428+- Using lifetime parameters (`<'a>`) on types that should use `<S: BosStr>`
429429+430430+**WRONG PATTERN (rewrite):**
431431+- Taking ownership (`Did`, `Post`) in function parameters instead of borrowing (`&Did`, `Did<&str>`, `&Post<CowStr<'_>>`)
432432+- Using `.into_output()` when `.parse::<CowStr<'_>>()` would suffice (data processed in same scope)
433433+- Using `#[serde(borrow)]` on BosStr-parameterized fields
434434+- Missing `#[serde(bound(deserialize = "S: Deserialize<'de> + BosStr"))]` on custom types
435435+- Calling `.into_static()` on types that are already `SmolStr`-backed
436436+- Using `FromStr::parse()` on validated types
437437+- Roundtripping through `String` or `to_string()`
438438+439439+**WILL NOT COMPILE:**
440440+- Custom types with `S: BosStr` missing `IntoStatic` derive
441441+- Dropping response while holding borrowed `CowStr<'_>` parse
442442+- Missing `Unknown` variant in union matches
443443+- Forgetting `extra_data` field in manual construction
444444+445445+**SECURITY:**
446446+- Not verifying issuer in OAuth flows
447447+- Not validating DID document ID matches request
448448+- Reusing DPoP proofs across requests
449449+- Not implementing JTI tracking in server code
450450+451451+452452+## Documentation
453453+454454+**Always read the docs before implementing:**
455455+- [jacquard](https://docs.rs/jacquard/latest/jacquard/)
456456+- [jacquard-common](https://docs.rs/jacquard-common/latest/jacquard_common/)
457457+- [jacquard-api](https://docs.rs/jacquard-api/latest/jacquard_api/)
458458+- [jacquard-oauth](https://docs.rs/jacquard-oauth/latest/jacquard_oauth/)
459459+- [jacquard-identity](https://docs.rs/jacquard-identity/latest/jacquard_identity/)
460460+461461+**LLMs.txt for comprehensive patterns:**
462462+- https://tangled.org/@nonbinary.computer/jacquard/raw/main/llms.txt
463463+464464+465465+## Example Patterns
466466+467467+### Making an XRPC Call
468468+469469+```rust
470470+use jacquard::prelude::*;
471471+use jacquard::api::app_bsky::feed::get_author_feed::GetAuthorFeed;
472472+473473+let request = GetAuthorFeed::new()
474474+ .actor("alice.bsky.social".into())
475475+ .limit(50)
476476+ .build();
477477+478478+let response = agent.send(request).await?;
479479+let output = response.into_output()?; // SmolStr-backed, owned
480480+481481+for item in output.feed {
482482+ println!("{}", item.post.author.handle);
483483+}
484484+```
485485+486486+### Creating a Record
487487+488488+```rust
489489+use jacquard::prelude::*;
490490+use jacquard::api::app_bsky::feed::post::Post;
491491+492492+let post = Post::builder()
493493+ .text("Hello ATProto!")
494494+ .created_at(Datetime::now())
495495+ .build();
496496+497497+agent.create_record(post, None).await?;
498498+```
499499+500500+### Identity Resolution
501501+502502+```rust
503503+use jacquard::identity::PublicResolver;
504504+505505+let resolver = PublicResolver::default();
506506+507507+// Handle -> DID
508508+let did = resolver.resolve_handle(&handle).await?;
509509+510510+// DID -> PDS endpoint
511511+let pds = resolver.pds_for_did(&did).await?;
512512+```
513513+514514+### Custom Type with BosStr
515515+516516+```rust
517517+use jacquard::common::types::BosStr;
518518+use jacquard::common::types::value::Data;
519519+use jacquard::common::DefaultStr;
520520+use jacquard_derive::IntoStatic;
521521+use serde::{Serialize, Deserialize};
522522+use smol_str::SmolStr;
523523+use std::collections::BTreeMap;
524524+525525+#[derive(Serialize, Deserialize, IntoStatic, Debug, Clone)]
526526+#[serde(
527527+ rename_all = "camelCase",
528528+ bound(deserialize = "S: Deserialize<'de> + BosStr"),
529529+)]
530530+struct MyRecord<S: BosStr = DefaultStr> {
531531+ name: S,
532532+ count: u32,
533533+ #[serde(flatten, default, skip_serializing_if = "Option::is_none")]
534534+ extra_data: Option<BTreeMap<SmolStr, Data<S>>>,
535535+}
536536+```
537537+538538+### Zero-Copy Response Processing
539539+540540+```rust
541541+use jacquard::common::CowStr;
542542+543543+let response = agent.send(request).await?;
544544+545545+// Zero-copy: borrow strings directly from response buffer
546546+let output = response.parse::<CowStr<'_>>()?;
547547+process_immediately(&output);
548548+// response and output dropped together — no allocations
549549+550550+// If you need to keep it: convert to owned
551551+let owned = output.into_static();
552552+```
553553+554554+555555+## Philosophy
556556+557557+**Jacquard is designed for correctness, performance, and ergonomics — in that order:**
558558+559559+1. **Borrow first, own when needed** — Use `&str`, `CowStr<'_>`, and references by default. Own (`SmolStr`) only when data must outlive its source.
560560+2. **The default is a floor, not a ceiling** — `SmolStr` (= `DefaultStr`) makes ownership painless, but good code still prefers borrowed deserialization and borrowed parameters.
561561+3. **Validation at construction** — Invalid inputs fail fast at `new()`, not deep in application logic.
562562+4. **Caller chooses backing type** — The `S: BosStr` parameter lets you pick the right trade-off per call site.
563563+5. **Batteries included, but replaceable** — High-level `Agent` for convenience, low-level primitives for control.
564564+565565+**When in doubt:** Borrow. Use `.parse::<CowStr<'_>>()`, pass `&Did` or `Did<&str>`, and only reach for owned types when the compiler tells you the data needs to live longer.