Transports (Low-Level)
--- title: "Transports (Low-Level)" description: "Low-level details on the core transport implementations in the Rust SDK." --- Use this page when you want to understand the transport layer itself. If you are building a normal native server or client, start with the dedicated native server or native client guide first. - `NostrClientTransport`: client-side direct transport - `NostrServerTransport`: server-side direct transport ## Why use the transport layer directly Use transports directly when you need to: - integrate with your own request loop - control announcement timing yourself - tune authorization and session behavior - embed the transport in higher-level abstractions ## Signer choice All transport constructors accept an existing signer, not just a newly generated one. - Use `generate()` for temporary identities in examples, tests, and short-lived sessions. - Use `from_sk()` when you need a stable identity backed by a pre-existing hex or `nsec` private key. ```rust use contextvm_sdk::signer; let signer = signer::from_sk("<hex-or-nsec-private-key>")?; ``` ## Low-level client transport example ```rust use contextvm_sdk::core::types::{JsonRpcMessage, JsonRpcRequest}; use contextvm_sdk::signer; use contextvm_sdk::transport::client::{ NostrClientTransport, NostrClientTransportConfig, }; #[tokio::main] async fn main() -> contextvm_sdk::Result<()> { let keys = signer::generate(); let mut transport = NostrClientTransport::new( keys, NostrClientTransportConfig::default() .with_relay_urls(vec!["wss://relay.damus.io".to_string()]) .with_server_pubkey("<server-hex-pubkey>"), ) .await?; transport.start().await?; let mut rx = transport.take_message_receiver().expect("receiver available"); transport .send(&JsonRpcMessage::Request(JsonRpcRequest { jsonrpc: "2.0".to_string(), id: serde_json::json!(1), method: "tools/list".to_string(), params: None, })) .await?; if let Some(message) = rx.recv().await { println!("received: {:?}", message); } transport.close().await?; Ok(()) } ``` ## Low-level server transport example ```rust use contextvm_sdk::core::types::{JsonRpcMessage, JsonRpcResponse}; use contextvm_sdk::signer; use contextvm_sdk::transport::server::{ NostrServerTransport, NostrServerTransportConfig, }; #[tokio::main] async fn main() -> contextvm_sdk::Result<()> { let keys = signer::generate(); let mut transport = NostrServerTransport::new( keys, NostrServerTransportConfig::default().with_announced_server(true), ) .await?; transport.start().await?; let mut rx = transport.take_message_receiver().expect("receiver available"); while let Some(req) = rx.recv().await { if let Some(id) = req.message.id() { transport .send_response( &req.event_id, JsonRpcMessage::Response(JsonRpcResponse { jsonrpc: "2.0".to_string(), id: id.clone(), result: serde_json::json!({"ok": true}), }), ) .await?; } } Ok(()) } ``` ## Server-side semantics to understand The server transport does more than relay bytes. It manages: - multi-client session state - request route storage - authorization via `allowed_public_keys` - allowlist bypasses via `CapabilityExclusion` - announcement publication - encryption negotiation and response mirroring Those behaviors are part of the server transport implementation and are exercised heavily by the integration tests. ## What the server receives Incoming traffic is delivered as `IncomingRequest`, which includes: - the parsed `JsonRpcMessage` - the client pubkey - the original Nostr request event id - whether the incoming message was encrypted That extra metadata is what allows correct response routing and encryption mirroring. --- _This page was ported from the [ContextVM Rust SDK repository](https://github.com/ContextVM/rs-sdk/tree/main/docs/transports.md)._Use this page when you want to understand the transport layer itself.
If you are building a normal native server or client, start with the dedicated native server or native client guide first.
NostrClientTransport: client-side direct transportNostrServerTransport: server-side direct transport
Why use the transport layer directly
Section titled “Why use the transport layer directly”Use transports directly when you need to:
- integrate with your own request loop
- control announcement timing yourself
- tune authorization and session behavior
- embed the transport in higher-level abstractions
Signer choice
Section titled “Signer choice”All transport constructors accept an existing signer, not just a newly generated one.
- Use
generate()for temporary identities in examples, tests, and short-lived sessions. - Use
from_sk()when you need a stable identity backed by a pre-existing hex ornsecprivate key.
use contextvm_sdk::signer;
let signer = signer::from_sk("<hex-or-nsec-private-key>")?;Low-level client transport example
Section titled “Low-level client transport example”use contextvm_sdk::core::types::{JsonRpcMessage, JsonRpcRequest};use contextvm_sdk::signer;use contextvm_sdk::transport::client::{ NostrClientTransport, NostrClientTransportConfig,};
#[tokio::main]async fn main() -> contextvm_sdk::Result<()> { let keys = signer::generate(); let mut transport = NostrClientTransport::new( keys, NostrClientTransportConfig::default() .with_relay_urls(vec!["wss://relay.damus.io".to_string()]) .with_server_pubkey("<server-hex-pubkey>"), ) .await?;
transport.start().await?; let mut rx = transport.take_message_receiver().expect("receiver available");
transport .send(&JsonRpcMessage::Request(JsonRpcRequest { jsonrpc: "2.0".to_string(), id: serde_json::json!(1), method: "tools/list".to_string(), params: None, })) .await?;
if let Some(message) = rx.recv().await { println!("received: {:?}", message); }
transport.close().await?; Ok(())}Low-level server transport example
Section titled “Low-level server transport example”use contextvm_sdk::core::types::{JsonRpcMessage, JsonRpcResponse};use contextvm_sdk::signer;use contextvm_sdk::transport::server::{ NostrServerTransport, NostrServerTransportConfig,};
#[tokio::main]async fn main() -> contextvm_sdk::Result<()> { let keys = signer::generate(); let mut transport = NostrServerTransport::new( keys, NostrServerTransportConfig::default().with_announced_server(true), ) .await?;
transport.start().await?; let mut rx = transport.take_message_receiver().expect("receiver available");
while let Some(req) = rx.recv().await { if let Some(id) = req.message.id() { transport .send_response( &req.event_id, JsonRpcMessage::Response(JsonRpcResponse { jsonrpc: "2.0".to_string(), id: id.clone(), result: serde_json::json!({"ok": true}), }), ) .await?; } }
Ok(())}Server-side semantics to understand
Section titled “Server-side semantics to understand”The server transport does more than relay bytes.
It manages:
- multi-client session state
- request route storage
- authorization via
allowed_public_keys - allowlist bypasses via
CapabilityExclusion - announcement publication
- encryption negotiation and response mirroring
Those behaviors are part of the server transport implementation and are exercised heavily by the integration tests.
What the server receives
Section titled “What the server receives”Incoming traffic is delivered as IncomingRequest, which includes:
- the parsed
JsonRpcMessage - the client pubkey
- the original Nostr request event id
- whether the incoming message was encrypted
That extra metadata is what allows correct response routing and encryption mirroring.
This page was ported from the ContextVM Rust SDK repository.