---
title: "Crossfire Client Libraries (JVM & Browser)"
description: "Besides the [Unity Crossfire Plugin](../crossfire), Namazu Crossfire ships two Java-based client libraries that speak the same [wire protocol](../crossfire-protocol-reference): a JVM/native client for desktop, server-to-server, and integration-test use, and a browser client compiled to JavaScript via TeaVM. Both sit on top…"
url: "https://namazustudios.com/docs/add-ons/crossplay-gaming-backend/crossfire-client-libraries/"
updated: "2026-08-14"
category: "Add-Ons / Crossplay"
---

# Crossfire Client Libraries (JVM & Browser)

Besides the [Unity Crossfire Plugin](../crossfire), Namazu Crossfire ships two Java-based client libraries that speak the same [wire protocol](../crossfire-protocol-reference): a JVM/native client for desktop, server-to-server, and integration-test use, and a browser client compiled to JavaScript via TeaVM. Both sit on top of a small shared API so [Application](https://namazustudios.com/glossary/application/) code can be written mostly against interfaces rather than against a specific [WebRTC](https://namazustudios.com/glossary/webrtc/) backend.

---

![Module relationship diagram: api and common feed client, which client-onvoid and client-teavm implement; both, plus the separate Unity plugin, speak the same wire protocol to the server](images/client-architecture.svg)

## The shared client API

The `client` module defines the vendor-neutral surface both implementations expose:

- **`Crossfire`** — the top-level facade. Obtain a builder from a `CrossfireClientProvider` (an SPI, discovered via `META-INF/services`), configure it, then `connect()`. Modes are combinations of `Protocol` (`WebRTC` or `SIGNALING`) and role (host or client) — e.g. `WEBRTC_HOST`. After connecting, call `findMatchHost()`/`findMatchClient()` (optionally scoped to a specific `Protocol`) to get the active peer collection, and subscribe to `onHostOpenStatus`/`onClientOpenStatus` for open/close notifications.
- **`SignalingClient`** — the underlying handshake/signal transport. Exposes `getState()` (match [id](https://namazustudios.com/glossary/id/), host, [Profile](https://namazustudios.com/glossary/profile/) roster), `backlog()` (the buffered signals described in the protocol reference), `signal(Signal)`, `control(ControlMessage)`, and three `handshake(...)` overloads (fire-and-forget, callback, or blocking with a timeout). Its own phase enum mirrors the server's connection state machine: `READY → CONNECTED → HANDSHAKING → SIGNALING → TERMINATED`.
- **`MatchHost`** — `start()`, `knownPeers()`, `findPeer(profileId)`, `newPeerQueue()`, `onPeerStatus(...)`, `close()`.
- **`MatchClient`** — `connect()`, `findPeer()` (there's only one — the host), `newPeerQueue()`, `onPeerStatus(...)`, `close()`.
- **`Peer`** — one connected participant. `getPhase()` (`READY`/`CONNECTED`/`TERMINATED`), `send(String)`/`send(ByteBuffer)` (returns a `SendResult`: `SENT`/`NOT_READY`/`ERROR`/`TERMINATED`), and `onMessage`/`onStringMessage`/`onError` subscriptions.

Implementation-agnostic configuration is expressed as plain records, so the same values can be handed to either backend:

- `CrossfireIceServer` — `urls`, `username`, `password`, `hostname`, `tlsCertPolicy`. `CrossfireIceServer.googleDefaults()` returns Google's public STUN servers.
- `CrossfireDataChannelConfig` — `ordered`, `negotiated`, `maxPacketLifeTime`, `maxRetransmits`, `id`, `protocol`. `defaults()` is ordered, non-negotiated, with no lifetime/retransmit caps.
- `CrossfireOfferOptions` — `voiceActivityDetection`, `iceRestart`. `defaults()` has VAD on, ICE restart off.
- `CrossfireTlsCertPolicy` — `SECURE` or `INSECURE_NO_CHECK`, for relaxing certificate validation against a local dev TURN/relay server using a self-signed certificate.

Both concrete implementations extend `AbstractCrossfire`, which handles the common bookkeeping: it subscribes to the `SignalingClient`'s signals, and on receiving a `HOST` signal it works out — per supported protocol — whether the local peer is the host or a client, then builds a fresh set of `MatchHost`/`MatchClient` instances via two hooks the subclass supplies (`populateHosts`/`populateClients`). [Application](https://namazustudios.com/glossary/application/) code normally never touches `AbstractCrossfire` directly.

## JVM client (`client-onvoid`)

Built on [`dev.onvoid.WebRTC:WebRTC-java`](https://github.com/devopvoid/webrtc-java) (native [WebRTC](https://namazustudios.com/glossary/webrtc/) bindings) plus the [Jakarta](https://namazustudios.com/glossary/jakarta/) WebSocket client API for the signaling transport. This is the client to reach for in a JVM game server, a headless bot, or an integration test — not for a browser deployment.

Get a builder from `OnvoidCrossfireClientProvider`:

```
Crossfire crossfire = new OnvoidCrossfireClientProvider()
    .newBuilder()
    .withDefaultUri(URI.create("wss://your-server/app/ws/crossfire"))
    .withIceServers(List.of(CrossfireIceServer.googleDefaults()))
    .withDataChannelConfig(CrossfireDataChannelConfig.defaults())
    .build();

crossfire.connect();
MatchHost host = crossfire.findMatchHost().orElseThrow();
```

If no URI is supplied, the builder falls back to the `ELEMENTS_CROSSFIRE_URI` environment variable, then the `dev.getelements.elements.crossfire.client.uri` system property.

**Host role:** `WebRTCMatchHost` listens for `CONNECT`/`DISCONNECT` signals and lazily creates one offering peer (`WebRTCOfferingPeer`) per remote [Profile](https://namazustudios.com/glossary/profile/) as they connect. **Client role:** `WebRTCMatchClient` wraps a single answering peer (`WebRTCAnsweringPeer`) targeting whichever [Profile](https://namazustudios.com/glossary/profile/) the `HOST` broadcast signal names. Both share one native `PeerConnectionFactory` (`SharedPeerConnectionFactory`) and a single serialized executor for native [WebRTC](https://namazustudios.com/glossary/webrtc/) calls (`SharedWebRTCExecutor`) by default — override with `withPeerConnectionFactory`/`withExecutor` only if you need per-instance isolation.

`CrossfireIceServer`, `CrossfireOfferOptions`, and `CrossfireDataChannelConfig` map onto onvoid's native `RTCIceServer`/`RTCOfferOptions`/`RTCDataChannelInit` types, including the TLS cert policy. Note that offer options only flow to the host (offering) side — the answering side always uses onvoid's default `RTCAnswerOptions`.

## Browser client (`client-teavm`)

Compiles the same client abstractions to JavaScript using [TeaVM](https://teavm.org/), calling the browser's native `WebSocket` and `RTCPeerConnection` directly through thin JSO overlays — no separate JS SDK to keep in sync with the Java protocol model. Get a builder from `TeaVMCrossfireClientProvider`; usage mirrors the JVM client:

```
Crossfire crossfire = new TeaVMCrossfireClientProvider()
    .newBuilder()
    .withDefaultUri(URI.create("wss://your-server/app/ws/crossfire"))
    .withIceServers(List.of(CrossfireIceServer.googleDefaults()))
    .build();

crossfire.connect();
```

The host/client peer roles work the same way as the JVM client (an offering peer per remote [Profile](https://namazustudios.com/glossary/profile/) on the host side, one answering peer on the client side), backed by plain `HashMap`s rather than concurrent collections, since generated JS is single-threaded.

**Known gaps versus the JVM client** — worth knowing before you assume feature parity:

- `withOfferOptions(...)` and `withDataChannelConfig(...)` are accepted by the builder but currently have **no effect** on the browser target — data channels are always created with a hardcoded `{"ordered":true}` configuration. Don't rely on custom retransmit/lifetime/negotiated settings in a browser build.
- `CrossfireIceServer.hostname` and `tlsCertPolicy` are silently dropped when building the browser's ICE server list — browsers manage their own TLS trust and don't expose a knob to override it, so `CrossfireTlsCertPolicy` has no meaning here.
- `newPeerQueue()` throws `UnsupportedOperationException` on both `MatchHost` and `MatchClient` in the browser build — a blocking queue would deadlock a single-threaded JS runtime. Use `onPeerStatus(...)` callbacks instead of polling a queue.

The `client-teavm` module pins an older Jetty version for its test/dev browser runner and excludes the Elements SDK BOM's newer Jetty transitives to avoid a classpath conflict — if you fork this module's build, keep that exclusion, or the embedded test server won't start.

## Choosing a client

| Target | Module | Use for |
| --- | --- | --- |
| Unity (desktop/console/mobile) | [Unity Crossfire Plugin](../crossfire) | Shipping games built on Unity Netcode for GameObjects |
| JVM (native [WebRTC](https://namazustudios.com/glossary/webrtc/)) | `client-onvoid` | Headless bots, load/integration tests, JVM-based game servers acting as a peer |
| Web browser | `client-teavm` | Browser-based game clients, browser-side test harnesses |
