Backend & architecture

Build a typed Rust Backend, generate clients, and understand the underlying Wire Protocol v1.

Verified September 11, 2026

On this page

When a Backend is useful#

UI-only Curios can already use Host capabilities through host.xxx. Put native computation, long-running tasks and public cross-Curio services in a Backend when needed.

Host manages installation, managed processes, windows, connection identity, routing, deadlines and cancellation. Public services belong to the resident Backend, independent of its window. They can start on demand, accept calls after ready, and remain available when an installed window closes. Backend-only Curios need no UI.

A private on-demand Backend normally exits when its window closes. If it owns work that must continue without the UI, it may declare the no-argument private method backgroundStatus and return { "active": boolean }. The Host queries it only on an already-connected Backend: true retains the Backend, while false performs normal cleanup. Host exit, disabling or uninstalling the Curio, and ending a development session still stop it. The value must reflect the Backend's real task state, not UI state.

Authors choose their private business architecture. The following sections distinguish optional private RPC, public services and the Host process connection.

Configure the process#

Run the wizard with the name Native Tool, choose Rust and install frontend dependencies. Rust/Cargo and the macOS build toolchain are required:

sh
trove new
cd native-tool
trove generate
trove check
trove dev

trove generate exports the Rust contract and creates src/generated/backend.ts; do this before the first read-only check. Dev and pack subsequently prepare bindings automatically.

Business logic lives in backend/src/main.rs; vendor/trove-backend-0.0.1-alpha.1 handles the protocol, and its nested contract/ contains the shared Rust contract declaration library. tools/build.mjs builds dist/backend/curio-backend and the UI. Backend.contract references the generated contracts/backend.json. Use trove kit to update the matching kit while preserving application files.

For a manually attached process, use external development mode and point TROVE_DEV_SESSION_FILE at this session’s .trove/dev-session.json; the kit reads connection details. Session shutdown sends shutdown to external Backends but does not forcibly kill manually launched processes. The Host does not distribute Node/Python runtimes. Other languages can implement Wire directly.

Connect using Wire Protocol v1#

Host-managed Backends connect over a Unix Domain Socket using Wire v1 and complete hello → welcome → ready. Stdout/stderr are for logs, not protocol messages.

The complete Wire v1 reference covers environment variables, framing, handshake fields, every message, bidirectional calls, identity, deadlines/cancellation, subscriptions, capacity limits and shutdown. It also provides a downloadable implementation. Individual message JSON Schemas cannot express these runtime rules.

The Rust kit handles this connection layer. Other languages can implement it directly; an existing internal HTTP/WebSocket channel may coexist.

Implement private requests#

In the generated Rust project, this is a complete backend/src/main.rs:

rust
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use trove_backend::{Backend, Context, Result, Service};

#[derive(Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
struct Input { text: String }
#[derive(Serialize, JsonSchema)]
struct Output { text: String }

#[tokio::main]
async fn main() -> Result<()> {
    Backend::new(Service::new("dev.local.native-tool.backend", "0.0.1-alpha.1")
        .method("uppercase", 30_000, |_ctx: Context, input: Input| async move {
            println!("Converting {} characters", input.text.chars().count());
            Ok(Output { text: input.text.to_uppercase() })
        }))
        .run().await
}

The kit exports the contract from actual Input/Output types and handles framing, concurrency, deadlines, cancellation and shutdown. Authors maintain Rust types and handlers; JSON Schema and frontend types are generated artifacts. After generation, the frontend calls:

ts
import { createClient } from "./generated/backend.js";

const device = createClient();
console.log((await device.uppercase({ text: "hello" })).text);

Private contracts serve @self only. A public capability needs .provide(service), an exported service contract and a services.provides declaration. Backend.contract alone never publishes it.

Handlers can inspect caller, remaining and is_cancelled or await cancelled on Context. Context::request forwards the parent context so Host can inherit its trace and remaining deadline. Host authenticates the reverse call as the Backend making it; it does not impersonate the original UI caller. Move blocking FFI and CPU work to application workers with cooperative cancellation. Other languages must preserve request IDs, return exactly one of result/error, and handle out-of-order completion and cancellation.

Verify the protocol and lifecycle#

In the Rust starter, run:

sh
trove test

Basic mode checks hello, fragmented frames, eight concurrent unknown-method errors, and normal shutdown/goodbye. It does not prove successful business calls, execution cancellation, events or reverse calls.

Full mode uses trove test --cases protocol-cases.json. Cases must describe real success, errors, a cancellable wait and a reverse call; Backends advertising subscribe also need subscription cases. Download the fixture source and complete cases for an isolated test project; see protocol validation.

Internal communication: recommendations and freedom#

Trove does not mandate a backend.xxx object or a particular UI business layer. You can:

  • Adopt the template’s optional private contract generator and name its client device, api, tasks or any domain name. It routes to @self and handles connection/types for you.
  • Keep your own HTTP, WebSocket, message bus or application services, managing their authentication, connections, cancellation and types yourself. Internal operations need no Trove Contract unless exposed through Trove RPC.
  • Expose public Backend services with no private RPC, or build a UI that only calls Host.

Backend.contract describes only private RPC exposed through @self; bindings.backend only selects the optional generated file. Neither defines your internal architecture. Cross-Curio calls still target public Backend capabilities declared in services.provides. They cannot reach another Curio’s @self or open its UI through a service call.

A Host-managed process currently still needs the Wire lifecycle connection, even when internal business traffic uses HTTP. Rust’s Backend::default().run() can supply that connection without private services. A plain HTTP entry without Wire handshake will time out during startup; there is no Wire-free managed-process mode today.

Public services without private RPC#

This complete Rust main.rs needs no placeholder private Service:

rust
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use trove_backend::{Backend, Context, Result, Service};

#[derive(Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
struct Input { text: String }
#[derive(Serialize, JsonSchema)]
struct Output { text: String }

#[tokio::main]
async fn main() -> Result<()> {
    Backend::default()
        .provide(Service::new("dev.example.text-tools", "0.0.1-alpha.1")
            .method("uppercase", 5_000, |_ctx: Context, input: Input| async move {
                Ok(Output { text: input.text.to_uppercase() })
            }))
        .run().await
}

Set the Curio ID to dev.example.text-tools and declare that service, version and contracts/text-tools.json in services.provides. This example only provides uppercase; it does not implement the progress event used elsewhere. Build the binary and export its actual contract:

sh
./dist/backend/curio-backend --export-contract contracts/text-tools.json --service dev.example.text-tools

Include the export in your bindings.generate workflow, and include the contract and executable in package.files. Consumers generate types from that contract and acquire a reference by Curio ID.

Backend::new(private) remains available for private RPC; Backend::default().provide(public) supports public-only services. Backend::contract() returns Result<Value>; without a private service it returns self.unavailable. Use the service export path for public contracts. For a non-Rust Backend, see the standalone Rust contract exporter; the contract language does not restrict the implementation language.