Login is decided in PreAuth by one auth resource named in the config
Status: Accepted (2026-11-08)
Context
How players log in (as guests, by device only, with a password and a second factor, against a whitelist or an external service) is a decision of each server, not of the platform. The synchronous player_connecting event, which runs before a slot is allocated (ADR 0038), cannot carry it: a login decision may wait for the player to fill in a form in game or for an external service to answer, and an in-game form needs a live session. Meanwhile, gameplay resources must not see a player whose login is not decided.
Decision
- PreAuth comes after
ServerHello. The connection holds a slot and a session token; it counts towardmax_playersand the per-IP limit.player_connectingstays where it is, before the slot, for cheap synchronous refusals (name, lists). Everything a join does today (the connect events, broadcasts, streaming) happens at the moment the connection is accepted. - One auth resource, named in
server.toml.[auth] resourcenames it; without the key every player is a guest and joins at once. A name that is not in[resources] startupstops the server from starting. A resource never declares itself the auth resource. - A separate pending handle. The event
player_auth(pending)reaches only the auth resource's VM.pendingis its own type with a narrow surface (id, name, IP,accept,reject, and what later login features add to it), not aplayer. A handler that decides nothing defers the decision; a handler error refuses the connection. Other resources learn about the player only from the normalplayer_connectafter the accept. - Fail-closed. A decision must come within
[auth] timeout_seconds. While the auth resource is not running, a new connection getsConnectRejectauth_unavailable; stopping or reloading the resource disconnects both the waiting connections and the accepted players, since nothing vouches for them any more. A connection refused in PreAuth getsKickNotify, and noplayer_disconnectis raised: for the other resources the player never existed. - Input in PreAuth is a whitelist. Only connection-level packets are taken (a repeated
ClientHello, acks, pongs,ClientBye, andRequestSpawnas a keepalive that is never granted), plus what the login screen needs (ADR 0055). Everything else is dropped before it can observe any state. The full list is in ServerHello, §PreAuth.
Consequences
- The wire gains the
ConnectRejectreason 9,auth_unavailable, and theauth_pendingflag inServerHello. - The console
playerscommand lists waiting connections markedpre-auth; the player count in the server query counts taken slots, waiting ones included. - The pending handle grows only with the login features that need it (the credential of ADR 0053, the login screen and web login of ADR 0055).
Alternatives considered
- Waiting before
ServerHello. Without a session there is no in-game login screen and no keepalive, and the handshake would need a pending state of its own. Rejected. - Replacing
player_connectingwith the auth gate. Cheap synchronous refusals (name, lists) would lose their place before the slot. Rejected. - A flag in
resource.toml. Starting any resource would change the login policy, and "named but not running" could not be expressed. Rejected. - A normal
playerhandle with a filter. Every one of dozens of gameplay calls andspawn_playerwould need a gate. Rejected. - Deciding through the handler's return value. A second path to the same result next to a later
acceptorreject. Rejected. - A timeout fixed in the core, or left to the resource. A host with an external second factor could not raise the former; a bug in the resource would hold a slot forever with the latter. Rejected.
- Refusing without an auth resource through the
gamemodereason with a text. A core condition would pose as a gamemode decision. Rejected.
