Authority
When a player sends a command that targets an entity (move their character, pick up an item, fire a weapon), the server needs to verify that the sender actually controls that entity and isn’t manipulating someone else’s. Authority is how golem-engine enforces that boundary.
Authenticate the realtime upgrade
Section titled “Authenticate the realtime upgrade”Use the upgrade request to validate a session token before the transport connection is accepted. The golem/auth helper extracts a query parameter while your game owns token parsing, database lookup, and the value stored in Session.Data:
srv.OnUpgrade(auth.UpgradeHandler(auth.UpgradeOptions{ Validate: func(r *http.Request, token string) (any, error) { account, err := accounts.ValidateSessionToken(token) if err != nil { return nil, err } return account, nil },}))
srv.OnConnect(func(sess *golem.Session) { account := sess.Data.(*accounts.Account) connectPlayer(account)})The default query parameter is token; set TokenParam to use another name. Missing or blank tokens are rejected without calling Validate. Validator errors reject the upgrade and remain compatible with errors.Is. Do not include secrets in validator errors because the listener logs the error and sends its text in the HTTP 401 response.
Client bootstrap helpers append the same query values without hand-building URLs; see the JavaScript, Go, and Unity client guides.
Spawn a session avatar
Section titled “Spawn a session avatar”Ownership is tied to a session. For the main player entity, SpawnAvatar combines ownership, optional field-of-interest setup, typed lookup, and disconnect cleanup:
srv.OnConnect(func(sess *golem.Session) { player := player.New(sess.Data.(*accounts.Account)) if err := srv.SpawnAvatar(sess, player, golem.AvatarOptions{ FOIRadius: 500, FOIMargin: 10, }); err != nil { log.Printf("spawn player: %v", err) }})Set FOIRadius to zero when interest management is disabled. A positive radius requires ServerConfig.CellSize > 0. Duplicate avatar spawns for one session return an error instead of replacing the live entity.
Look up the avatar without maintaining a separate session-to-entity map:
player, ok := golem.AvatarOf[*player.Player](srv, sess.ID)Avatar returns the general golem.Entity form when the concrete type is not known.
For other owned entities, pass the session ID to CreateEntity directly:
pet := synced.NewSyncedPet(x, y)_ = srv.CreateEntity(pet, sess.ID)Without the second argument, the entity is unowned. World NPCs, pickups, and environmental objects typically work this way. Unowned entities cannot be targeted by entity-scoped commands.
How CommandRouter uses it
Section titled “How CommandRouter uses it”When a client sends an entity-targeted command, CommandRouter.Dispatch checks that senderID matches the owning session for the target entity. If it does, the handler fires. If not, Dispatch returns an error and the handler is skipped entirely, so no game code runs.
This means you don’t need to write ownership checks inside handlers. Since OnMessage (and therefore Dispatch) runs on the tick goroutine, entity mutations inside command handlers are also safe without additional locking:
router.OnMove(func(senderID int64, ent *synced.SyncedPlayer, cmd *synced.MoveCommand) { // only reached when senderID owns ent, so this is safe to apply x, y := ent.Position() ent.SetPosition(x+cmd.Dx, y+cmd.Dy)})Transferring ownership
Section titled “Transferring ownership”Sometimes a player reconnects with a new session (their WebSocket was dropped and re-established). Use srv.SetOwner to move the entity to the new session without recreating it:
srv.OnConnect(func(sess *golem.Session) { if existingEntityID, ok := lookupPlayerEntity(sess.PlayerToken); ok { // Reconnect: transfer the existing entity to the new session srv.SetOwner(existingEntityID, sess.ID) } else { // First connect: create a new entity player := synced.NewSyncedPlayer(0, 0, "Aria") _ = srv.CreateEntity(player, sess.ID) }})SetOwner returns false if the entity ID doesn’t exist; ownership is unchanged in that case.
If the entity schema contains visibility: owner fields, ownership transfer also updates what eligible clients may retain. On the next replication pass, a previous owner that still knows the entity receives public full state that clears private values. The new owner receives authoritative full state when the entity is visible to that session. Command authority changes as soon as SetOwner returns.
Ownership transfer does not change avatar identity. SpawnAvatar is intended for session-lifetime avatars that are removed at disconnect; use direct ownership APIs for persistent reconnectable entities.
Querying ownership
Section titled “Querying ownership”Use srv.Owner(entityID) to find out which session currently owns an entity:
if sessionID, ok := srv.Owner(player.EntityID()); ok { // sessionID is the current owner}This is occasionally useful for server-side logic that needs to know which session is responsible for a given entity, for example to send a targeted message to a specific client with srv.Send.
Cleanup on disconnect
Section titled “Cleanup on disconnect”An avatar created with SpawnAvatar is deleted automatically after your OnDisconnect callback. Its FOI and session indexes are cleared at the same time. The callback runs first, so it can still read the avatar:
srv.OnDisconnect(func(sess *golem.Session) { if player, ok := golem.AvatarOf[*player.Player](srv, sess.ID); ok { saveCharacter(player) }})Manual DeleteEntity remains safe if the avatar dies before disconnect. For owned entities created directly with CreateEntity, the game still decides whether to delete them or transfer ownership.
See Client commands for the full command dispatch flow and Command schemas for how target: entity is declared in YAML.