Docs / Clients

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 QUIC

Supported 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

  1. 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.
  2. The descriptor endpoint is host:port, so construct quic://host:port for ClientConfig.url. The transport value is quic.
  3. 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.
  4. 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.