Skip to content

Minimal server wiring

After golem-bake, a typical main constructs an golem.Server through the generated Go integration, registers gameplay callbacks, and calls Run. The generated runtime attaches the removal serializer, command router, event broadcaster, and snapshot fingerprint helpers so your entrypoint only has to describe game-specific behavior. Imports below use placeholder module paths—replace them with your module and generated package paths.

package main
import (
"context"
"log"
"golem-engine/golem"
"example.com/mygame/internal/synced" // generated go-server integration
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
srv, rt := synced.NewServer(golem.ServerConfig{
TickRate: 20,
Addr: ":8080",
StaticDir: "./public", // optional: serve static files (maps, sprites) over HTTP
})
srv.OnTick(func(dt float64, s *golem.Server) {
// Your simulation: read/write entities via s (CreateEntity, Get, DeleteEntity, …).
_ = dt
})
srv.OnConnect(func(sess *golem.Session) {
// Player joined — sess.ID is the optional owner argument to CreateEntity(..., sess.ID)
})
rt.Commands.BindAllHandlers(func(sess *golem.Session, err error) {
log.Printf("session %d command: %v", sess.ID, err)
})
srv.OnDisconnect(func(sess *golem.Session) {
// Clean up: remove the player's entity (srv.DeleteEntity(...)).
})
if err := srv.Run(ctx); err != nil && err != context.Canceled {
log.Fatal(err)
}
}

Notes:

  • Addr enables integrated networking. When set, Run starts the HTTP server plus the selected transport endpoint alongside the tick loop and auto-broadcasts entity updates to all connected clients after each tick.
  • Transport selects the integrated transport. The default is golem.TransportWebTransport; use golem.TransportWebSocket with StateUpdateLaneStream when you want WebSocket.
  • Path overrides the transport endpoint path. The defaults are /ws for WebSocket and /wt for WebTransport.
  • TLSCertFile / TLSKeyFile or DevSelfSignedCert are required when you use WebTransport, because browsers require HTTP/3 + TLS for WebTransport sessions.
  • With DevSelfSignedCert, golem also allows loopback cross-port WebTransport origins for local development, so a page like http://localhost:8080 can connect to https://localhost:4433. Outside that mode, WebTransport stays same-origin by default unless you override it with SetWebTransportCheckOrigin.
  • StaticDir is optional. When set, non-transport HTTP requests are served from that directory—useful for sprites, audio, or any static assets your client needs.
  • MapDir is optional. When set, files from that directory are served at /maps/—designed for Tiled (.tmj) and LDtk (.ldtk) map files. See Map file serving and World schemas for the url_prefix workflow that pairs with this option.
  • CellSize enables interest management. When set to a positive value, each client receives only entities near its avatar instead of a full broadcast. See the interest management page for AssignFOI / RemoveFOI wiring.
  • The generated NewServer helper calls SetRemovalSerializer for you. If you construct golem.NewServer directly, removals still require the generated serializer.
  • With generated JS clients, the generated command router should normally bind DispatchPacket to the reliable stream. The JS runtime batches one or more encoded commands into a ClientPacket, so the raw reliable payload is no longer a single ClientMessage. That logical packet shape stays the same regardless of whether the transport carried it inside a WebSocket message or a WebTransport stream write.
  • If you opt into WebTransport datagrams, register OnDatagram for the lossy lane separately; generated commands still travel through OnMessage on the reliable lane.
  • Cancel ctx (e.g. on SIGINT) for clean shutdown—Run then returns ctx.Err().
  • With commands, use rt.Commands.BindAllHandlers from generated code—see Client commands.

With go-server commands, reliable-stream and reliable-datagram handlers can share one error callback:

srv, rt := synced.NewServer(golem.ServerConfig{Addr: ":8080"})
// rt.Commands.OnMove(...); rt.Commands.OnPing(...); etc.
rt.Commands.BindAllHandlers(func(sess *golem.Session, err error) {
log.Printf("session %d command: %v", sess.ID, err)
})

Use Dispatch() only when your transport still hands you a single encoded ClientMessage at a time, such as tests or a custom non-batched client path.

If you omit Addr, Run only ticks—no HTTP server is started. Mount the server’s transport handler on your own server (the same Server instance holds sessions, snapshots, and broadcast hooks you already registered with OnConnect / OnMessage / OnDisconnect):

import "net/http"
srv := golem.NewServer(golem.ServerConfig{TickRate: 20, Path: "/ws"})
mux := http.NewServeMux()
mux.Handle("/ws", srv.Handler())
go func() { _ = http.ListenAndServe(":8080", mux) }()
if err := srv.Run(ctx); err != nil && err != context.Canceled {
log.Fatal(err)
}

If you configure golem.TransportWebTransport, mount srv.Handler() on an HTTP/3 server instead of http.ListenAndServe. The same origin defaults apply there too: same-origin in normal mode, loopback cross-port when DevSelfSignedCert is enabled, or your own policy via srv.SetWebTransportCheckOrigin(...).

See also Game loop, Typical workflow, and Channels and transports.