fl-lobby REST API (v1)¶
This is the wire contract between a Fighters Legacy dedicated server (the registration client,
engine/net/LobbyRegistration), the in-game server browser (engine/net/LobbyListClient), and an
fl-lobby service. Hosting is self-host only: fl-lobby is a small standalone service (planned as a Go
companion repo, issue #999 / #36) that anyone can run. This document is the source of truth it is written
from — the C++ side in this repo already speaks it.
All bodies are JSON (Content-Type: application/json). All endpoints are versioned under /v1. There is
no authentication in v1: the lobby trusts the network it is exposed on, keys entries on the request's
source IP + advertised port, and caps its state (see Limits). NAT traversal is out of scope for v1 —
a server behind NAT that cannot be reached at its source IP simply will not be joinable.
Endpoints¶
POST /v1/servers — register / heartbeat¶
A dedicated server POSTs this on an interval ([lobby]-configured, default 30 s) while it wants to be
listed. The lobby upserts an entry keyed by (source IP, port) and refreshes its TTL. The server never
sends its own host — the lobby uses the request source IP.
Request body:
{
"name": "My Server", // display name
"port": 4778, // the GAME port a client connects to
"players": 3, // current player count
"max_players": 16, // capacity
"mode": "builtin:tdm", // game-mode id
"mission": "fjord", // current mission/map (may be empty)
"visibility": "public" // always "public"; a private server never POSTs
}
Response: 200 OK (or 201 Created on first registration). The body is ignored by the client.
DELETE /v1/servers — deregister¶
Sent best-effort on server shutdown to drop the entry immediately (rather than waiting for the TTL). The
lobby matches the entry by (source IP, port).
Request body:
{ "port": 4778 }
Response: 200 OK or 204 No Content. A missing entry is not an error.
GET /v1/servers — list¶
The in-game browser GETs this to list public servers. The response is a JSON array of server objects:
[
{
"name": "My Server",
"host": "203.0.113.7", // the address a client connects to (the lobby fills this from the source IP)
"port": 4778,
"mode": "builtin:tdm",
"mission": "fjord",
"players": 3,
"max_players": 16,
"passworded": false
}
]
The client parser is deliberately tolerant: it ignores unknown keys, accepts address as an alias for
host, drops any object with no host or a zero port, and bounds both the row count and per-string length
(see Limits). A server whose entry has gone stale should simply be absent from the list.
TTL and freshness¶
An entry is considered live for 2.5 × the server's heartbeat interval after its last POST. The
default heartbeat is 30 s, so a default entry expires ~75 s after the server stops heartbeating (e.g. a
crash with no DELETE). The lobby prunes expired entries lazily and never returns them from GET.
Limits (denial-of-service posture)¶
- At most 1024 entries returned from
GET /v1/servers(the client also caps at 1024 rows). - At most 1 MiB response body for
GET(the client caps its accumulated body at 1 MiB). - Per-string fields truncated to 256 bytes client-side; the lobby should bound them similarly.
- The lobby should rate-limit
POST/DELETEper source IP and cap total entries.
Federation¶
The client's lobby list is configured via [client] lobby_urls (comma-separated, empty by default):
the player opts in to a lobby. LAN discovery works with no lobby at all. There is no central registry — a
community runs its own fl-lobby and shares its URL. Multiple lobby URLs are merged in the browser, and LAN
entries win over lobby entries for the same host:port.
Versioning¶
The path is versioned (/v1). A breaking change ships as /v2; the client advertises which versions it
speaks by the path it requests. v1 is frozen once fl-lobby ships.