Getting Started
This guide shows how to use TrevRPC in an application. It follows the same high-level shape as ConnectRPC: define a protobuf service, generate typed bindings, implement the service, register it on a server, and call it with a generated client.
TrevRPC is not a Connect or gRPC wire-compatible runtime. It uses a custom protobuf protocol over native QUIC, ordinary HTTP/3, or WebTransport.
Requirements
- Protobuf definitions for your service.
- The TrevRPC runtime for your language.
- The matching TrevRPC generator for your language.
- TLS and QUIC configuration for native
trevrpc/1, ordinary HTTP/3, or WebTransport.
Use Local Development instead if you want to work on TrevRPC itself or run the checked-in examples.
1. Define a Service
Start with a normal protobuf service definition:
syntax = "proto3";
package example.greeter;
service Greeter {
rpc SayHello(HelloRequest) returns (HelloReply);
rpc LotsOfReplies(HelloRequest) returns (stream HelloReply);
rpc LotsOfGreetings(stream HelloRequest) returns (HelloReply);
rpc BidiHello(stream HelloRequest) returns (stream HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
TrevRPC supports unary, server-streaming, client-streaming, and bidirectional-streaming methods.
2. Generate TrevRPC Bindings
Install and run the matching generator for your language. With Buf, a minimal generation config looks like this:
version: v2
plugins:
- local: protoc-gen-trevrpc-rust
out: gen/rust
opt:
- runtime_path=::trevrpc
- local: protoc-gen-trevrpc-go
out: gen/go
opt:
- runtime_import=trev.zip/llc/trevrpc/trevrpc-go
- local: protoc-gen-trevrpc-cpp
out: gen/cpp
- local: protoc-gen-trevrpc-kotlin
out: gen/kotlin
opt:
- runtime_package=zip.trev.trevrpc
- local: protoc-gen-trevrpc-js
out: gen/js
opt:
- runtime_import=trevrpc-js
Generated code provides:
- A service trait or interface to implement for Rust, Go, and Kotlin servers.
- A typed client.
- A Rust, Go, or Kotlin registration function that connects your implementation to the TrevRPC server.
- C++20
{Service}Service,{Service}Client, andRegister{Service}bindings with synchronous methods and explicitResultvalues. - C++ bindings that use protobuf's generated C++ full or lite messages and adapt them to the byte-oriented
trevrpc-ctransport without protobuf-c. - Kotlin
.trevrpc.ktbindings that use protobuf Java messages, suspend functions, andFlow. - JavaScript
.trevrpc.jsclient files with companion.trevrpc.d.tsTypeScript declarations.
See Protobuf and Code Generation for generator options and language-specific output shapes.
3. Add the Runtime
For Rust, add the trevrpc runtime to your application crate using the package source your project uses for Trev packages. The default crate features include client, server, Quinn, HTTP/3 server, and WebTransport support.
For Go, add the runtime module:
go get trev.zip/llc/trevrpc/trevrpc-go
The Go runtime uses github.com/quic-go/quic-go for native QUIC and HTTP/3, plus github.com/quic-go/webtransport-go for WebTransport. The Rust runtime uses Quinn, h3, h3-quinn, and h3-webtransport for unified server handling; its native WebTransport client uses web-transport-quinn.
The C++20 runtime and generator build from source under trevrpc-cpp/. Generate normal protobuf C++ messages and TrevRPC C++ service bindings from the same schema, compile both generated outputs, and link them with trevrpc::cpp and the selected protobuf C++ runtime. Both full and lite protobuf are supported; C++ applications do not generate or link protobuf-c output. See C++ Guide.
The Kotlin modules currently build from source rather than from published Maven artifacts. Include trevrpc-kotlin in your Gradle build and depend on :core plus :transport-netty for JVM native QUIC and HTTP/3, or :transport-cronet for Android-oriented HTTP/3. See Kotlin Guide.
For JavaScript or TypeScript browser clients, add the runtime package:
npm install trevrpc-js
The JavaScript runtime uses the browser WebTransport API and is currently client-side only.
4. Implement the Server
Rust generated services are async traits. A unary method implementation looks like this:
struct GreeterService;
#[trevrpc::async_trait]
impl greeter::Greeter for GreeterService {
async fn say_hello(
&self,
request: greeter::HelloRequest,
) -> Result<greeter::HelloReply, trevrpc::Status> {
Ok(greeter::HelloReply {
message: format!("hello, {}", request.name),
})
}
}
Go generated services are interfaces:
type greeterService struct{}
func (greeterService) SayHello(_ context.Context, request *greeter.HelloRequest) (*greeter.HelloReply, error) {
return &greeter.HelloReply{Message: "hello " + request.Name}, nil
}
Kotlin generated services use suspend functions and Flow:
class GreeterServiceImpl : GreeterService {
override suspend fun sayHello(
context: RequestContext,
request: HelloRequest,
): HelloReply =
HelloReply.newBuilder()
.setMessage("hello ${request.name}")
.build()
}
C++ generated services are synchronous {Service}Service interfaces. Unary and client-streaming handlers return Result<Response>; server-streaming and bidirectional handlers return Status after writing responses through typed stream adapters.
Return trevrpc::Status in Rust or *trevrpc.Status in Go when you need a specific RPC status code. Kotlin handlers throw TrevRpcException(Status...). C++ handlers return errors through Result.
5. Register Routes
Generated registration functions install handlers on a TrevRPC server.
Rust:
let mut server = trevrpc::server::Server::new();
greeter::register_greeter(&mut server, GreeterService);
Go:
server := trevrpc.NewServer()
greeter.RegisterGreeterServer(server, greeterService{})
Kotlin:
val server = Server()
registerGreeter(server, GreeterServiceImpl())
C++ registration uses Register{Service}(Server&, std::shared_ptr<{Service}Service>) and returns Result<void>. The server's route adapters retain the shared service implementation until the server closes.
Set authorization, metrics, and server limits before serving traffic.
6. Configure a Transport
Native TrevRPC over QUIC expects TLS ALPN trevrpc/1.
Rust:
server_crypto.alpn_protocols = vec![trevrpc::ALPN.to_vec()];
client_crypto.alpn_protocols = vec![trevrpc::ALPN.to_vec()];
Go:
tlsConfig := &tls.Config{
NextProtos: []string{trevrpc.ALPN},
}
Applications own certificate selection, trust roots, server-name verification, and mTLS policy.
Kotlin Netty endpoints use NettyServerTls and NettyClientTls. NettyRpcServerConfig advertises trevrpc/1, h3, or both according to the enabled transports. Keep hostname verification enabled and use explicit trust roots in production.
WebTransport uses HTTP/3 WebTransport configuration instead of the native TrevRPC ALPN. See WebTransport for Rust and Go setup.
Ordinary HTTP/3 uses ALPN h3, an HTTP POST, and the same TrevRPC frames in the request and response bodies. See HTTP/3 for server setup.
7. Serve Traffic
Rust serves a caller-created Quinn endpoint:
server.serve_quinn(endpoint).await?;
Use serve_quinn_with_shutdown when you have a shutdown future and want graceful draining.
Go serves a quic-go listener:
if err := trevrpc.ServeQUIC(ctx, listener, server); err != nil {
log.Fatal(err)
}
Cancel the serving context to begin shutdown.
Kotlin binds a single Netty UDP listener:
val listener = NettyRpcServer.bind(
server,
NettyRpcServerConfig(
bindAddress = InetSocketAddress("127.0.0.1", 50051),
tls = NettyServerTls.Pem(File("server-key.pem"), File("server-cert.pem")),
enableNative = true,
enableHttp3 = true,
),
)
Call listener.shutdown() to stop admission, drain active work, and release transport resources.
The C++ Server is a synchronous facade over the trevrpc-c server transport. Create it with Server::listen, register services before calling the blocking serve(), and call shutdown() before destroying server state.
8. Create a Client
Generated clients use a long-lived channel that reconnects for future work. Every RPC snapshots one ready connection generation and never moves from it. If that generation dies, the RPC fails without retry, replay, or resumption. New calls fail immediately with Unavailable or its runtime equivalent while the channel reconnects; call the runtime readiness method first when waiting is intentional.
Rust:
let channel = trevrpc::client::Channel::connect(
endpoint.clone(),
addr,
"localhost",
).await?;
let client = greeter::GreeterClient::new(channel.clone());
let reply = client
.say_hello(
greeter::HelloRequest { name: "TrevRPC".into() },
trevrpc::client::CallOptions::new()
.with_timeout(std::time::Duration::from_secs(5)),
)
.await?;
Go:
channel, err := trevrpc.Dial(ctx, addr, trevrpc.DialOptions{TLSConfig: tlsConfig})
if err != nil {
log.Fatal(err)
}
defer channel.Close()
client := greeter.NewGreeterClient(
channel,
trevrpc.WithTimeout(5*time.Second),
)
reply, err := client.SayHello(ctx, &greeter.HelloRequest{Name: "TrevRPC"})
C native MsQuic:
trevrpc_config config = trevrpc_default_config();
trevrpc_channel* channel = NULL;
int rc = trevrpc_channel_connect(
"127.0.0.1", 50051, &config, NULL, 5000000000ull, NULL, &channel);
if (rc != 0) {
return rc;
}
Hello__V1__HelloRequest request = HELLO__V1__HELLO_REQUEST__INIT;
request.name = "TrevRPC";
Hello__V1__HelloReply* reply = NULL;
rc = hello_v1_greeter_say_hello(channel, &request, &reply);
if (reply != NULL) {
hello__v1__hello_reply__free_unpacked(reply, NULL);
}
trevrpc_channel_close(channel);
trevrpc_channel_release(channel);
Generated C methods accept trevrpc_channel* directly. Call trevrpc_channel_wait_ready before new work when the application intentionally waits through reconnection. Routine C channel coverage is native MsQuic QUIC; single-connection native QUIC and WebTransport APIs are advanced interfaces in trevrpc_raw.h. Always close, then release, the channel.
C++20 clients use the same underlying byte transport through a long-lived Channel, but accept and return protobuf C++ messages:
auto connected = trevrpc::Channel::connect("127.0.0.1", 50051, config, 5s);
auto channel = std::move(connected).value();
hello::v1::GreeterClient client(channel);
hello::v1::HelloRequest request;
request.set_name("TrevRPC");
auto reply = client.SayHello(request);
if (!reply) {
std::cerr << reply.error().message() << '\n';
}
Share the Channel across generated {Service}Client instances. A call remains pinned to the ready connection generation on which it starts and fails if that generation is lost; it is never retried, replayed, resumed, or moved. Reconnection only enables future calls. Wait for readiness only when the application intentionally wants to wait through reconnection, then close the channel during shutdown. See C++ Guide for all four synchronous RPC shapes.
Kotlin Netty native QUIC:
val config = NettyQuicClientConfig(
remoteAddress = InetSocketAddress("127.0.0.1", 50051),
tls = NettyClientTls("localhost", trustCertificates = listOf(certificate)),
)
val channel: RpcChannel = NettyRpcChannel.nativeQuic(config)
channel.awaitReady()
val client = GreeterClient(channel, CallOptions(timeout = 5.seconds))
val reply = client.sayHello(
HelloRequest.newBuilder().setName("TrevRPC").build(),
)
Use NettyRpcChannel.http3(config) for JVM HTTP/3. Android applications use CronetRpcChannel.create(engine, origin, callbackExecutor) with an application-owned CronetEngine and executor. Kotlin does not currently provide a WebTransport client.
At shutdown, call channel.close().
JavaScript and TypeScript WebTransport clients use generated .trevrpc.js modules:
import { connect } from "trevrpc-js";
import { GreeterClient } from "./hello/v1/greeter.trevrpc.js";
const channel = await connect(
"https://127.0.0.1:50051/trevrpc",
);
const client = new GreeterClient(channel, {
timeoutMs: 5000,
metadata: { authorization: "Bearer trevrpc-example-token" },
});
try {
const reply = await client.sayHello({ name: "TrevRPC" });
} finally {
channel.close({ closeCode: 0, reason: "client shutdown" });
}
The browser root connect API returns a Channel over WebTransport. Node uses the same root API through package exports, or Channel from trevrpc-js/node, backed by native MsQuic. Use waitUntilReady() before issuing new calls when the application intentionally waits through a reconnect.
Raw single-connection transports are advanced-only and intended for integrations, benchmarks, diagnostics, and deterministic tests. See Connection Lifecycle for the complete routine and advanced API list, readiness, shutdown, migration, and TLS-resumption behavior.
9. Set Production Defaults
For real applications, configure these before exposing a server:
- TLS certificates, trust roots, and the selected transport negotiation.
- Client call deadlines.
- Server concurrency and stream limits.
- Request metadata authorization or transport-level identity.
- Metrics hooks.
- Graceful shutdown behavior.
- Channel readiness and reconnect behavior.
See Operations, Limits, and Security for the available knobs and defaults.
Next Steps
- Use Protobuf and Code Generation to wire generation into your build.
- Read Rust Guide, Go Guide, C++ Guide, Kotlin Guide, or JavaScript and TypeScript Guide for complete language API details.
- Read WebTransport if you need HTTP/3 WebTransport transport support.
- Read HTTP/3 if you need ordinary HTTP/3 server or Kotlin client support.
- Read Streaming if your service uses streaming methods.
- Read Connection Lifecycle for channel generation, reconnection, and advanced raw-transport semantics.
- Read Protocol and Wire Format before implementing a transport or debugging interoperability.