This file provides context for Claude Code to understand the node bindings crate structure and development workflows.
This crate (bindings_node) provides Node.js bindings for libxmtp using NAPI-RS. It exposes the core XMTP functionality to JavaScript/TypeScript applications.
The crate follows a domain-driven modular structure:
| Module | Purpose |
|---|---|
client/ |
Client struct and all its methods, split by concern |
conversations/ |
Conversations collection - listing, creating, streaming |
conversation/ |
Conversation methods, split by concern |
content_types/ |
Content type definitions (text, reaction, reply, etc.) |
messages/ |
Message struct and related types |
| File | Contains |
|---|---|
lib.rs |
Crate entry point, ErrorWrapper utility |
client/mod.rs |
Client struct definition, core methods |
client/create_client.rs |
create_client() function, client builder |
client/options.rs |
ClientOptions configuration |
conversations/mod.rs |
Conversations struct, core methods |
conversation/mod.rs |
Conversation struct, core methods |
messages/mod.rs |
Message structs and enums |
Large structs like Client and Conversation have their impl blocks split across multiple files:
client/
├── mod.rs # Client struct + core methods
├── consent_state.rs # impl Client { consent methods }
├── signatures.rs # impl Client { signature methods }
└── ...
Each file adds methods to the same struct via impl Client { ... } blocks. This keeps files focused and manageable.
conversation/= methods on a single Conversation (send message, get members, etc.)conversations/= methods on the collection (list, create, stream conversations)
- Identify the concern (consent, signatures, identity, etc.)
- Find or create the appropriate file in
src/client/ - Add the method in an
impl Clientblock with#[napi]attribute
// src/client/my_feature.rs
use napi_derive::napi;
use crate::client::Client;
#[napi]
impl Client {
#[napi]
pub async fn my_new_method(&self) -> napi::Result<()> {
// implementation
Ok(())
}
}- Add
mod my_feature;tosrc/client/mod.rs
Same pattern as Client - add to src/conversation/ in appropriate file.
- Create
src/content_types/<type_name>.rs - Define the struct with
#[napi(object)] - Add
pub mod <type_name>;tosrc/content_types/mod.rs - Add variant to
ContentTypeenum inmod.rs - Update
DecodedMessageContentType,DecodedMessageContentInner, andDecodedMessageContent - Add encoder function if content type should be sent by clients
- Add function to expose
ContentTypeIdstruct if content type will be read by clients
Create a root-level file in src/ for types used across multiple modules.
From the repository root, use just recipes (preferred — they use Nix automatically):
just node install # Install dependencies
just node check # Build release NAPI bindings
just node lint # Check TypeScript formatting
just node format # Format TypeScript files
just node test # Run tests (requires just backend up)
just node build # Alias for checkUse ErrorWrapper to convert Rust errors to NAPI errors:
use crate::ErrorWrapper;
self.inner_client
.some_method()
.await
.map_err(ErrorWrapper::from)?;#[napi]on struct/impl - exports to JavaScript#[napi(object)]- plain object (not a class)#[napi(getter)]- property getter#[napi]on method - exported method
Use napi::bindgen_prelude::BigInt for nanosecond timestamps to avoid precision loss.
Conversation uses a pattern where create_mls_group() creates a new MlsGroup from stored data, since the inner group isn't clonable in a way that makes sense for NAPI.
Tests are in test/ directory as TypeScript files using Vitest:
Client.test.tsConversation.test.tsConversations.test.ts
Run tests with yarn test. Requires local XMTP node (start with ./dev/up from repo root).
napi/napi-derive- Node.js bindingsxmtp_mls- Core MLS implementationxmtp_db- Database layerxmtp_proto- Protocol buffer definitions
mod.rs- Module entry point, contains primary struct<concern>.rs- Additional impl blocks for that concern<type>.rsin content_types/ - Individual content type definitions