Java SDK
Create and dial rstream tunnels from Java services and JVM backends.
The Java SDK is built for JVM services that should own their rstream tunnel lifecycle from application code. It reads the same config file as the CLI and other SDKs, opens the engine control channel, creates bytestream tunnels, serves HTTP directly from accepted streams, forwards streams to existing local services when needed, dials private tunnels by name or ID, and verifies webhook deliveries in backend receivers.
github.com/rstreamlabs/rstream-javaJava SDK for framework-free tunnel runtimes, private bytestream dialing, shared config resolution, and webhook verification.This page focuses on the Java runtime surface. For raw HTTP routes, see APIs. For webhook delivery semantics, see Webhooks.
The Go SDK remains the reference SDK and covers the broadest protocol surface today. The Java SDK is intentionally framework-free: Spring, Jakarta EE, Micronaut, Quarkus, and other framework integrations should live in application code or examples, while the core SDK stays small and usable from any JVM service.
Use it when the JVM process is already the natural owner of the service lifecycle. A backend service can publish an in-process HTTP server, a device agent can maintain a private tunnel for command traffic, and an operations service can dial a private resource without spawning the CLI or passing sockets through another supervisor.
SDK Surface
The first Java SDK surface focuses on bytestream workflows:
| Surface | Status |
|---|---|
| Runtime client and control channel | Available |
| Published HTTP/1.1 bytestream tunnels | Available |
| Private bytestream dial by name or ID | Available |
| Direct HTTP handler | Available |
| Local TCP forwarding to an existing service | Available |
Direct stream accept and CompletableFuture helpers | Available |
| Shared YAML config and primary environment variables | Available |
| Token and mTLS agent authentication | Available |
| Webhook signature verification and event parsing | Available |
| Datagram tunnels, QUIC transport, HTTP/3 runtime | Not supported yet |
| SOCKS, custom DNS, external credential stores | Rejected explicitly |
Unsupported transport and credential-storage settings fail during configuration resolution instead of being ignored. This keeps Java services predictable when they share the same ~/.rstream/config.yaml file as the CLI or another SDK.
Choosing a Pattern
The SDK exposes small closeable primitives rather than a framework adapter. Pick the path that matches the ownership boundary in your application.
| Pattern | Use it when |
|---|---|
BytestreamTunnel.serveHttp() | A JVM process should serve HTTP requests in-process through a tunnel. |
createTunnel() plus forwardTo() | A JVM process should publish an existing local TCP or HTTP service. |
BytestreamTunnel.accept() | Application code should own each accepted stream directly. |
RstreamClient.dial() | Java code should connect to a private tunnel and speak a bytestream protocol itself. |
Webhooks.event() | A Java backend receives signed project lifecycle webhooks. |
The common thread is that rstream remains part of the application lifecycle. The process decides when the tunnel exists, which labels describe it, how shutdown is handled, and whether public access is published or kept private.
Runtime Model
The public stream API is blocking and closeable, which fits normal Java service code. Internally, the client uses background workers for the control channel, accepted streams, forwarding, and async helpers. RstreamClient.connect() opens a control channel. The control channel creates published or private tunnels. Published tunnels accept streams from the rstream edge and can serve HTTP handlers in-process or forward to a local TCP service that already exists. Private tunnels are consumed by RstreamClient.dial() and return RstreamStream directly to application code.
Keep the control channel open for as long as the tunnels it owns should remain available. Closing the channel or tunnel tears down the runtime state in the engine; it does not depend on a separate CLI process staying alive. Use try-with-resources for clients, control channels, and streams so shutdown remains explicit:
import io.rstream.CreateTunnelOptions;
import io.rstream.HttpVersion;
import io.rstream.RstreamClient;
import io.rstream.TunnelProtocol;
public final class PublishedTunnel {
public static void main(String[] args) {
try (var client = RstreamClient.fromEnv();
var control = client.connect()) {
var tunnel =
control.createTunnel(
CreateTunnelOptions.builder()
.name("java-api")
.protocol(TunnelProtocol.HTTP)
.httpVersion(HttpVersion.HTTP_1_1)
.publish(true)
.build());
System.out.println("Forwarding address: " + tunnel.forwardingAddress());
tunnel.close();
}
}
}When the engine and token are already known, create the client directly instead of reading the local config file:
import io.rstream.ClientOptions;
import io.rstream.RstreamClient;
var client =
RstreamClient.fromEnv(
ClientOptions.builder()
.engine("project-endpoint.cluster.example.rstream.test:443")
.readConfigFile(false)
.token(System.getenv("RSTREAM_AUTHENTICATION_TOKEN"))
.build());Install
Maven:
<dependency>
<groupId>io.rstream</groupId>
<artifactId>rstream</artifactId>
<version>0.1.0</version>
</dependency>Gradle:
implementation("io.rstream:rstream:0.1.0")The SDK targets Java 17 and newer.
Authentication and Project Setup
The Java SDK uses the same runtime config model as the rstream CLI.
Typical developer-machine setup:
rstream login
rstream project use <project-endpoint>The SDK then reads the same ~/.rstream/config.yaml file and context selection model as the CLI. Explicit client options are read first, then environment variables, then the selected context from the config file.
Token authentication and mTLS are mutually exclusive for the engine control channel. If an explicit engine override is set, stored credentials from another context are refused unless the credential is also provided explicitly. That fail-closed behavior prevents a token from a selected CLI context from being sent to an unrelated engine by accident.
For hosted projects, pass projectEndpoint or set it in the selected config context. The SDK can resolve the managed project through the Control plane API before opening the runtime connection.
For automation and containers, pass engine, token, and readConfigFile(false) explicitly when the runtime should not depend on a developer-machine config file. For local development, RstreamClient.fromEnv() keeps Java behavior aligned with rstream login and rstream project use.
Published HTTP Tunnel
Use this path when Java application code should handle HTTP requests directly. The call to serveHttp() returns a CompletableFuture<Void> and keeps accepting remote streams until the tunnel closes or serving fails.
import io.rstream.CreateTunnelOptions;
import io.rstream.HttpVersion;
import io.rstream.RstreamClient;
import io.rstream.RstreamHttpResponse;
import io.rstream.TunnelAuth;
import io.rstream.TunnelProtocol;
import java.net.InetAddress;
import java.util.Map;
public final class PublishedHttpTunnel {
public static void main(String[] args) throws Exception {
try (var client = RstreamClient.fromEnv();
var control = client.connect()) {
var tunnel =
control.createTunnel(
CreateTunnelOptions.builder()
.name("java-api")
.protocol(TunnelProtocol.HTTP)
.httpVersion(HttpVersion.HTTP_1_1)
.labels(Map.of("service", "api", "runtime", "java"))
.publish(true)
.auth(TunnelAuth.builder().token(true).rstream(true).build())
.build());
System.out.println("Forwarding address: " + tunnel.forwardingAddress());
tunnel
.serveHttp(
request -> RstreamHttpResponse.text(200, InetAddress.getLocalHost().getHostName()))
.join();
}
}
}The direct HTTP adapter handles fixed-length and chunked HTTP/1.1 request bodies. Header size, body size, and read timeout limits can be tightened when the receiving service has stricter operational requirements:
import io.rstream.RstreamHttpOptions;
import java.time.Duration;
tunnel.serveHttp(
request -> RstreamHttpResponse.text(200, "ok"),
RstreamHttpOptions.builder()
.maxHeaderBytes(16 * 1024)
.maxBodyBytes(1024 * 1024)
.readTimeout(Duration.ofSeconds(10))
.build());Managed Local Forwarding
Use forwarding when the service already binds a local TCP or HTTP port. New JVM framework integrations should prefer serveHttp() when the process can handle requests in-process.
var forwarding = tunnel.forwardTo("127.0.0.1", 8000);
forwarding.join();Direct Accept
Use direct accept when the application should handle each incoming stream itself instead of forwarding everything to a local port.
try (var client = RstreamClient.fromEnv();
var control = client.connect()) {
var tunnel =
control.createTunnel(
CreateTunnelOptions.builder()
.name("private-java")
.publish(false)
.build());
while (!tunnel.closed()) {
try (var stream = tunnel.accept()) {
stream.outputStream().write("ok".getBytes(StandardCharsets.UTF_8));
stream.outputStream().flush();
}
}
}Private tunnels do not accept public exposure options such as protocol(), httpVersion(), public authentication policy, trusted IPs, or GeoIP policy. Create the private bytestream tunnel with publish(false), then dial it by name or ID.
accept(Duration) returns null when no stream arrives before the timeout. acceptAsync() returns a CompletableFuture<RstreamStream> for services that already coordinate work through futures.
Private Dial
Private tunnels are dialed by tunnel name or ID and return a closeable byte stream. Application code can layer HTTP, a custom binary protocol, or another bytestream protocol on top of the returned stream.
import io.rstream.RstreamClient;
import java.nio.charset.StandardCharsets;
public final class PrivateDial {
public static void main(String[] args) throws Exception {
var target = args.length == 0 ? "private-api" : args[0];
try (var client = RstreamClient.fromEnv();
var stream = client.dial(target)) {
stream.outputStream().write("ping".getBytes(StandardCharsets.UTF_8));
stream.outputStream().flush();
System.out.println(new String(stream.inputStream().readNBytes(4), StandardCharsets.UTF_8));
}
}
}The dial options can ask the client to wait for an explicit engine response before writing application data. Keep the default unless the caller needs that extra acknowledgement for its protocol or diagnostics.
Webhook Receivers
The SDK also includes helpers for Java backends that receive rstream webhooks. Verify the raw request body against the rstream-signature header before parsing or storing the event. The returned event id is stable and can be used as the receiver-side idempotency key, and the event payload is exposed as Jackson JsonNode for normal JSON tree access.
Webhook payloads use the same project lifecycle events exposed by the platform. They are useful when an external system needs durable state around tunnels that are otherwise runtime resources: last seen timestamps, device online/offline status, inventory reconciliation, or automation triggered by tunnel creation and deletion.
import io.rstream.Webhooks;
var event = Webhooks.event(payloadBytes, signatureHeader, signingSecret);
var resourceId = event.object().path("id").asText("");
System.out.println(event.type().wireValue() + " " + event.id() + " " + resourceId);Use tunnel labels when webhook receivers need to build persistent application state around runtime resources that may appear and disappear frequently.
Examples
The repository includes runnable examples for the common integration paths:
| Example | Use it for |
|---|---|
examples/published-http-server | Serve HTTP directly from accepted rstream streams. |
examples/forward-local-port | Forward a published tunnel to an existing local TCP service. |
examples/private-dial | Dial a private bytestream tunnel by name or ID. |
examples/webhook-receiver | Verify signed webhook deliveries with the JDK HTTP server. |
examples/spring-boot-published-http | Serve Spring Boot application code directly from rstream streams. |
examples/spring-boot-webhook-receiver | Verify signed webhook deliveries in a Spring Boot controller. |
examples/quarkus-published-http | Serve Quarkus CDI application code directly from rstream streams. |
examples/micronaut-published-http | Serve Micronaut application code directly from rstream streams. |
examples/vertx-published-http | Serve Vert.x application code without blocking the event loop. |
Each example is deliberately small and uses the same configuration model as production code. Start with the one that matches the integration boundary, then move engine, token, labels, and endpoint policy into explicit application configuration as the deployment matures.
Environment variables
The Java SDK follows the runtime resolution model used by the CLI. Explicit SDK options are read first, then environment variables, then the selected context from RSTREAM_CONFIG or the default ~/.rstream/config.yaml file.
| Variable | Purpose |
|---|---|
RSTREAM_CONFIG | Path to the rstream configuration file. If unset, the SDK falls back to ~/.rstream/config.yaml. |
RSTREAM_CONTEXT | Name of the context to select during config-based resolution. |
RSTREAM_ENGINE | Engine endpoint used by the Java tunnel runtime. |
RSTREAM_ENGINE_ADDRESS | Compatibility alias for older local SDK workflows. Prefer RSTREAM_ENGINE in new code. |
RSTREAM_AUTHENTICATION_TOKEN | Authentication token used by token-authenticated runtime connections. |
RSTREAM_MTLS_CERT_FILE | Client certificate file for mTLS agent authentication. |
RSTREAM_MTLS_KEY_FILE | Client private key file for mTLS agent authentication. |
RSTREAM_API_URL | Control plane API URL for managed project discovery. |
RSTREAM_AUTHENTICATION_TOKEN and the mTLS certificate/key pair are mutually exclusive for the agent control-channel connection. When both mTLS variables are set, config-derived tokens are not used for that connection.