Channels and transports
Channels and transports
Section titled “Channels and transports”When you turn on integrated networking, golem-engine can carry the game protocol over WebTransport by default, or over WebSocket when you choose the stream state-update lane. WebTransport exposes a reliable stream plus datagram lanes, so most games can keep fast-changing state off the reliable stream while world data, snapshots, events, and generated commands keep their usual meaning.
Choosing a transport
Section titled “Choosing a transport”| Transport | Reliable lane | Datagram lanes | Default path | Browser URL |
|---|---|---|---|---|
| WebSocket | Yes | No | /ws | ws:// or wss:// |
| WebTransport | Yes | Unreliable and reliable command/custom datagrams | /wt | https:// |
WebTransport is the default because it supports the recommended datagram state-update lane. WebSocket remains available when you explicitly use StateUpdateLaneStream; it needs no transport-specific client config beyond the URL.
Native Go clients can use WebSocket or WebTransport through golem-go-client. Unity clients can use WebSocket by default, or WebTransport through the GolemWebTransportTransport adapter when the Unity project includes the io.demiurgos.webtransport package. Unity WebTransport clients receive the same compact datagram state updates as Go and JavaScript clients. If a Unity project uses WebSocket or a custom transport without datagrams, configure the server with StateUpdateLaneStream.
Configuring the server
Section titled “Configuring the server”Pick the integrated transport in ServerConfig. WebTransport is the default, so you only need Transport when switching to WebSocket:
srv, rt := synced.NewServer(golem.ServerConfig{ TickRate: 20, Addr: ":4433", DevSelfSignedCert: true, // local development only})
rt.Commands.BindAllHandlers(func(sess *golem.Session, err error) { log.Println("command:", err)})
srv.OnDatagram(func(sess *golem.Session, data []byte) { // Handle one lossy client datagram.})For production WebTransport, configure TLSCertFile and TLSKeyFile instead of DevSelfSignedCert. If you leave Path empty, WebSocket uses /ws and WebTransport uses /wt.
For WebSocket, also select the stream state-update lane:
srv := golem.NewServer(golem.ServerConfig{ Addr: ":8080", Transport: golem.TransportWebSocket, StateUpdateLane: golem.StateUpdateLaneStream,})When DevSelfSignedCert is enabled, golem also relaxes WebTransport origin checks for loopback development so a page like http://localhost:8080 can connect to https://localhost:4433. Outside that dev mode, WebTransport stays same-origin by default.
If your browser client learns the WebTransport URL through an HTTP API, serve the built-in bootstrap handler after the server is ready:
mux := http.NewServeMux()mux.Handle("/api/realtime-config", srv.RealtimeConfigHandler(golem.RealtimeConfigOptions{ PublicURL: "https://localhost:4433/wt",}))
go func() { if err := srv.Run(ctx); err != nil && err != context.Canceled { log.Fatal(err) }}()if err := srv.WaitReady(ctx); err != nil { log.Fatal(err)}The JSON includes the transport kind, public URL, optional state-datagram ACK timing, and certificate hashes only when they are needed for generated development self-signed WebTransport certificates:
{ "transport": "webtransport", "url": "https://localhost:4433/wt", "serverCertificateHashes": [ { "algorithm": "sha-256", "value": "0123456789abcdef..." } ], "eventualAckIntervalMs": 50}Browser clients pass serverCertificateHashes to the WebTransport constructor when the field is present. With publicly trusted TLS certificates, such as LetsEncrypt, the field is omitted and browsers use normal WebPKI validation. Native Go clients can call golemclient.FetchRealtimeConfig and golemclient.ConnectOptionsFromRealtimeConfig; the Go client hex-decodes any hashes and pins the WebTransport TLS certificate automatically when no explicit TLSClientConfig is set.
Most games should omit eventualAckIntervalMs and use the client default. Raising it can reduce tiny acknowledgement packets on busy WebTransport connections by giving outgoing state or command traffic more time to carry the acknowledgement with it. The tradeoff is slower delivery feedback under packet loss, so use higher values only after measuring a noisy connection or high packet rate.
Diagnosing connection failures
Section titled “Diagnosing connection failures”Transport logs include the transport kind, redacted URL, and where possible a short phase or reason. For WebTransport, dial or phase=connect means the client failed before opening Golem’s reliable stream; check the URL, TLS certificate, HTTP/3 listener, origin policy, and UDP reachability before looking at generated gameplay code.
For Unity, phase=open_stream or phase=stream_header_write means the WebTransport session connected but Golem’s reliable stream setup failed. For WebSocket, unclean closes include the close code and reason when the platform provides them.
WebTransport origins
Section titled “WebTransport origins”Browsers send the page origin on every WebTransport CONNECT request, including the page port. If your game page is served from https://game.example.com:8080 and WebTransport listens on https://game.example.com:4433/wt, configure the page origin explicitly:
srv := golem.NewServer(golem.ServerConfig{ Addr: ":4433", Transport: golem.TransportWebTransport, TLSCertFile: "cert.pem", TLSKeyFile: "key.pem", WebTransportAllowedOrigins: []string{ "https://game.example.com:8080", },})For deployments where the page and WebTransport endpoint always share a hostname but not necessarily a port, enable same-host origin mode instead:
srv := golem.NewServer(golem.ServerConfig{ Addr: ":4433", Transport: golem.TransportWebTransport, TLSCertFile: "cert.pem", TLSKeyFile: "key.pem", WebTransportAllowSameHostOrigin: true,})Same-host mode only relaxes the port check for HTTPS origins with the same hostname as the WebTransport request. For anything more custom, use srv.SetWebTransportCheckOrigin(...) and return true only for origins your game trusts.
Reliable lane
Section titled “Reliable lane”The reliable lane is where the built-in protocol lives:
- per-tick entity updates when
StateUpdateLaneStreamis selected - world snapshots and world updates
- server events
- generated client commands sent through
client.send()
The logical cap for one reliable message is 32000 bytes on both transports. That cap applies to each decoded ServerMessage or ClientPacket, not to every underlying transport write.
On the server side, golem first builds a per-session batch of logical frames, then lets the active transport emit that batch efficiently:
- WebSocket coalesces adjacent logical frames into ordered binary WebSocket messages targeting 32000 bytes each.
- WebTransport coalesces adjacent logical frames into ordered writes on one reliable stream. Each logical frame stays length-prefixed inside the stream, so a max-sized 32000-byte logical frame becomes a slightly larger framed write because of its 4-byte prefix.
A large tick can still become multiple ordered WebSocket messages or multiple ordered WebTransport stream writes. The game protocol does not attach meaning to those transport boundaries; clients decode the same logical frame sequence either way.
That shared model is why your server command path stays the same:
_, rt := synced.NewServer(golem.ServerConfig{Addr: ":4433"})
rt.Commands.BindStreamHandler(func(sess *golem.Session, err error) { log.Println("command:", err)})DispatchPacket is the normal entrypoint for the generated JS and Go clients because those clients batch one or more logical commands into one reliable ClientPacket before sending.
On the browser side, the current JS WebTransport client still dispatches decoded reliable frames one logical frame at a time after stream framing is removed. Server-side transport coalescing therefore reduces write overhead and queue pressure without changing the logical update flow seen by game code.
WebTransport datagrams
Section titled “WebTransport datagrams”WebTransport adds datagram lanes for traffic that should not sit behind the reliable stream. Use raw unreliable datagrams for high-rate, short-lived data where only the newest sample matters more than guaranteed delivery. Use reliable command/custom datagrams for small messages that must arrive without sharing stream ordering.
The WebTransport datagram cap used by golem is 1200 bytes, and protocol headers reduce the usable payload below that. The raw unreliable lane currently carries up to 1178 bytes of application payload, reliable unordered datagrams carry up to 1176 bytes, and reliable ordered datagrams carry up to 1174 bytes.
On the server:
srv.OnDatagram(func(sess *golem.Session, data []byte) { // Parse your own datagram payload here.})
if err := srv.SendUnreliable(sess.ID, payload); err != nil { log.Println("datagram:", err)}Use srv.BroadcastUnreliable(data) when the same lossy datagram should go to every connected WebTransport session. On a transport that has no datagram lane, the Go API returns golem.ErrUnreliableNotSupported.
On the browser client:
client.sendUnreliable(bytes);client.sendUnreliable() only sends when the active transport exposes an unreliable lane. On a WebSocket connection it is a no-op.
Connecting from the browser
Section titled “Connecting from the browser”For WebSocket, a URL string is still enough:
client.connect(`ws://${location.hostname}:8080/ws`);For WebTransport, pass transport-aware options:
client.connect({ transport: "webtransport", url: "https://localhost:4433/wt", serverCertificateHashes: [ { algorithm: "sha-256", value: "0123456789abcdef..." }, ],});Use serverCertificateHashes for local development or another explicit certificate pin. For publicly trusted TLS certificates, omit it and let the browser use WebPKI validation. value can be an ArrayBuffer, a typed-array view, or a hex string.
Connecting from native Go
Section titled “Connecting from native Go”Native Go clients can consume the same realtime config endpoint:
cfg, err := golemclient.FetchRealtimeConfig(ctx, "http://localhost:8080/api/realtime-config", nil)if err != nil { log.Fatal(err)}
options, err := golemclient.ConnectOptionsFromRealtimeConfig( cfg, golemclient.WithQueryParam("token", sessionToken),)if err != nil { log.Fatal(err)}
if err := client.Connect(ctx, options); err != nil { log.Fatal(err)}Use WithQueryParam or WithQuery for game-specific authentication and routing values. If you need a custom TLS policy, pass WithTLSClientConfig; otherwise any ServerCertificateHashes from realtime config are used for WebTransport certificate pinning.
If you need a custom origin policy, set it explicitly:
srv.SetWebTransportCheckOrigin(func(r *http.Request) bool { return r.Header.Get("Origin") == "https://example.com"})Certificate hashes for WebTransport
Section titled “Certificate hashes for WebTransport”When the server runs WebTransport, srv.WebTransportCertificateHashes() returns the certificate digests known by the integrated listener:
hashes := srv.WebTransportCertificateHashes()for _, hash := range hashes { log.Printf("%s %x", hash.Algorithm, hash.Value)}In a real app you usually rely on RealtimeConfigHandler. It includes hashes for generated development certificates by default and omits them for publicly trusted TLS certificates.
Custom mounting
Section titled “Custom mounting”srv.Handler() returns the active transport endpoint for custom routers.
For WebSocket, mount it on a normal HTTP server.
For WebTransport, create your own *http3.Server, point its Handler at your mux, then pass that server into srv.WebTransportServer(h3) before serving:
mux := http.NewServeMux()mux.Handle("/wt", srv.Handler())
h3 := &http3.Server{ Addr: ":4433", Handler: mux, TLSConfig: http3.ConfigureTLSConfig(&tls.Config{ Certificates: []tls.Certificate{cert}, }),}
wt := srv.WebTransportServer(h3)if err := wt.ListenAndServe(); err != nil { log.Fatal(err)}golem configures the HTTP/3 server for WebTransport session handling. You still own the TLS certificates, the final TLSConfig, and the server lifecycle.
For caller-owned HTTP/3 servers, origin policy still comes from golem. The same defaults apply: same-origin in normal mode, loopback cross-port in DevSelfSignedCert mode, or your own policy via srv.SetWebTransportCheckOrigin(...).
See also State updates, Client commands, JavaScript client, Go client, and Minimal server wiring.