Clients
Rust client
The published woven-client Rust SDK supports the Woven Host managed flow. This guide uses native QUIC, verified TLS, Bearer authentication, admission, subscription, publish, receive, and shutdown.
Managed-compatible · native QUICSupported managed surface
- Verified native QUIC with ClientTlsConfig and normal certificate chain, validity, and hostname/IP SAN checks
- Explicit Bearer authentication through Client::connect_with_tls_and_auth
- Managed admission with request_admission and the queue status, heartbeat, claim, and cancel operations
- Bounded admission scheduling through admit_with_cancellation
- Space subscription, server-assigned entities, reliable events, receive deadlines, and bounded graceful shutdown
- Message-only logger().info/warn/error and log(), with session/connection metadata in Host Logs
Install the client
Install the published woven-client 0.3.0 SDK from crates.io with cargo add woven-client@0.3.0 woven-protocol@0.3.0; the protocol crate supplies types used below. Source-path development remains supported: the example below uses sibling Woven/application checkouts instead of registry dependencies. Pin a reviewed source revision for that option.
[dependencies]
woven-client = { path = "../woven/crates/woven-client-rust" }
woven-protocol = { path = "../woven/crates/woven-protocol" }
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
serde_json = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync", "time"] }Host descriptor to client transport
- After recent account reauthentication, request POST /v1/servers/:id/connection with the server's current generation. The authenticated Host response is the connection descriptor; do not put its token in a URL or log it.
- The descriptor endpoint is host:port, so construct quic://host:port for ClientConfig.url. The transport value is quic.
- Keep namespaceId, sessionId, spaceId, epoch, and channelIds as canonical nonzero decimal u64 strings in JSON. Parse them to u64 in Rust instead of converting them through a JavaScript number.
- Build ClientTlsConfig from descriptor caPem when supplied, or from a populated application-owned rustls::RootCertStore for a publicly trusted certificate. Supply roots explicitly and keep remote TLS verification enabled.
Managed flow
Connect from a Host descriptor
use woven_client::{Client, ClientConfig, ClientTlsConfig};
use woven_protocol::AuthenticationScheme;
async fn connect(
descriptor: &serde_json::Value,
application_roots: Option<rustls::RootCertStore>,
) -> Result<(Client, u64, u64), Box<dyn std::error::Error>> {
let endpoint = descriptor["endpoint"].as_str().ok_or("missing endpoint")?;
let token = descriptor["token"].as_str().ok_or("missing token")?;
let namespace_id = descriptor["namespaceId"]
.as_str().ok_or("missing namespaceId")?.parse::<u64>()?;
let session_id = descriptor["sessionId"]
.as_str().ok_or("missing sessionId")?.parse::<u64>()?;
let tls = match descriptor["caPem"].as_str() {
Some(ca_pem) => ClientTlsConfig::from_ca_pem(ca_pem.as_bytes())?,
None => ClientTlsConfig::with_root_certificates(
application_roots.ok_or("no TLS roots supplied")?,
)?,
};
let client = Client::connect_with_tls_and_auth(
ClientConfig {
url: format!("quic://{endpoint}"),
token: token.to_owned(),
..ClientConfig::default()
},
tls,
AuthenticationScheme::Bearer,
)
.await?;
Ok((client, namespace_id, session_id))
}Wait for managed admission with a stop path
use std::time::Duration;
use woven_client::ManagedAdmissionOutcome;
use woven_protocol::{AdmissionStatus, QueueState};
let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>();
// Move cancel_tx to the owner of your explicit stop control and send ().
let cancellation = async move {
let _ = cancel_rx.await;
};
let (mut client, outcome) = client
.admit_with_cancellation(
namespace_id,
session_id,
"one-logical-attempt".to_owned(),
Duration::from_secs(30),
cancellation,
)
.await?;
match outcome {
ManagedAdmissionOutcome::Admission(result)
if result.status == AdmissionStatus::Admitted => {}
ManagedAdmissionOutcome::Queue(update)
if update.state == QueueState::Admitted => {}
other => return Err(format!("admission outcome: {other:?}").into()),
}Send a message to Host Logs
// After managed admission returns Admitted; no space subscription is needed.
client.logger().info("Scene loaded").await?;
client.logger().warn("Optional asset unavailable").await?;
client.logger().error("Failed to save progress").await?;
client.log("Info-level alias").await?;
// Sending is not a database acknowledgement. Continue receiving control traffic
// to observe server rejections. Host stores a rolling 200-event debug history.Subscribe and publish a reliable event on channel 1
use std::time::Duration;
use woven_protocol::{ControlPayload, MessagePayload};
let space = descriptor["spaces"].as_array()
.and_then(|spaces| spaces.first()).ok_or("missing space")?;
let space_id = space["spaceId"].as_str().ok_or("missing spaceId")?.parse::<u64>()?;
let space_epoch = space["epoch"].as_str().ok_or("missing epoch")?.parse::<u64>()?;
let has_channel_one = space["channelIds"].as_array()
.is_some_and(|ids| ids.iter().any(|id| id.as_str() == Some("1")));
if !has_channel_one {
return Err("channel 1 is not allowed for this space".into());
}
client
.subscribe_space(namespace_id, session_id, space_id, space_epoch, 1)
.await?;
let accepted = client.recv_timeout(Duration::from_secs(2)).await?
.ok_or("subscription response timed out")?;
if !matches!(accepted.message,
MessagePayload::Control(ControlPayload::SubscriptionAccepted(_))) {
return Err("subscription was not accepted".into());
}
let entered = client.recv_timeout(Duration::from_secs(2)).await?
.ok_or("entity assignment timed out")?;
if !matches!(entered.message,
MessagePayload::Control(ControlPayload::EntityEntered(_))) {
return Err("server did not assign an entity".into());
}
let entity_id = entered.entity_id.ok_or("missing assigned entity ID")?;
client
.publish_event(
namespace_id,
session_id,
space_id,
space_epoch,
1, // channel 1: ReliableOrdered / Ephemeral on the managed node
entity_id,
1, // sender sequence; increment for each event in this scope
1, // application-defined payload type ID
b"hello from Woven Host".to_vec(),
)
.await?;
client.close_gracefully(Duration::from_secs(2)).await?;Cancellation and bounds
- The complete DNS, TLS, and WVN1 handshake is bounded to 10 seconds. Each low-level admission or queue exchange is also bounded to 10 seconds.
- admit_with_cancellation requires a positive timeout of at most 15 minutes, owns the fresh pre-subscription client, sends bounded heartbeats, claims offers, and closes the connection on cancellation, deadline, or I/O failure.
- Admission and protocol rejections are returned as data or errors; the helper performs no transport retries. Do not silently retry a partial stream exchange or reuse a connection after cancellation.
- Use unique nonzero correlation IDs with the low-level queue API, reuse an idempotency key only for the same logical request on the same connection and scope, and keep publish sequences strictly increasing.
- Bound application concurrency, publish rate, payload size, receive waits, and shutdown time. A graceful close is a bounded opportunity to send close frames, not proof that the peer received pending data.
- Logging takes a nonempty message of at most 1,024 UTF-8 bytes, with 10 client logs per connection per second. The node attaches timestamp and connection/session scope; never log credentials. Host stores the latest 200 events per managed server, including automatic connections/disconnections, without logging ordinary updates. Collection is eventual and best-effort, not lossless archiving.
Availability
Managed native QUIC connectivity is implemented and validated in local Host/Woven end-to-end tests with verified TLS and Firebase emulators. This does not establish deployed-endpoint or browser-client validation. Pro and Dedicated are Preview, with private founder access first, and use this public Rust client contract.