Skip to content

Transports (Low-Level)

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

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

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.
use contextvm_sdk::signer;
let signer = signer::from_sk("<hex-or-nsec-private-key>")?;
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(())
}
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(())
}

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.

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.