net_backend

API reference

HTTP routes

"Auth" means Authorization: Bearer <access token> is required; a request without a valid token answers 401 (unauthorized / token_expired) or 403 banned before anything else happens. Every route can also answer 429 rate_limited, 500 internal and 503 unavailable; the tables list the other answers. Schemas are written as JSON with ? marking optional fields.

Shared shapes#

text
Ack                {}
Account            {"id":int, "email":string|null, "email_verified":bool, "display_name":string|null,
                    "roles":[string], "identities":[LinkedIdentity], "created_at":ms}
LinkedIdentity     {"provider":"steam", "subject":string}
TokenPair          {"token_type":"Bearer", "access_token":string, "access_expires_at":ms,
                    "refresh_token":string, "refresh_expires_at":ms}
AuthSession        {"account":Account, "tokens":TokenPair}
StorageObject      {"collection":string, "key":string, "owner":int, "value":JSON, "version":int,
                    "write":"owner"|"server", "updated_at":ms}
StorageObjectInfo  {"collection":string, "key":string, "version":int, "write":"owner"|"server",
                    "size_bytes":int, "updated_at":ms}
ObjectAck          {"collection":string, "key":string, "version":int, "updated_at":ms}
RoomInfo           {"id":int, "kind":"room"|"dm"|"group", "key"?:string, "name"?:string,
                    "member_count"?:int, "max_members"?:int, "peer"?:int}
ChatMessage        {"id":int, "room":int, "sender":int, "sender_name"?:string|null, "text":string,
                    "sent_at":ms, "nonce"?:string}
Page<T>            {"items":[T], "next_cursor"?:string}
  • RoomInfo.kind: room (public), dm (direct messages, with peer = the other user), group (members only). member_count counts users online in the room now; max_members is the cap in connections.
  • StorageObject.write: owner (the player may write it) or server (server-locked: player writes get 403).

Server#

Method Path Auth Request Answer
GET /v1/info no – {"protocol":1,"min_protocol":1,"modules":[string]}
GET /healthz no – {"status":"ok"}
GET /readyz no – {"status":"ready"}; 503 when not ready
GET /v1/openapi.json no – the OpenAPI document (when enabled)
GET /v1/asyncapi.json no – the AsyncAPI document (when enabled)
GET (upgrade) /v1/ws handshake header or first message – the WebSocket

Auth#

POST /v1/auth/register#

Create an account with email and password; it is logged in at once.

  • Auth: no.
  • Body: {"email":string, "password":string, "display_name"?:string}.
  • 200: AuthSession. A verification mail is sent.
  • Errors: 403 forbidden (registration closed, or refused by a server rule); 409 email_taken; 422 validation_failed (details per field); 429 rate_limited.

POST /v1/auth/login#

  • Auth: no.
  • Body: {"email":string, "password":string}.
  • 200: AuthSession.
  • Errors: 401 invalid_credentials; 403 banned (details until), email_not_verified, or a server rule's refusal; 429 rate_limited (too many attempts from this address or for this account); 503 unavailable (password hashing is saturated: retry shortly).

POST /v1/auth/steam#

  • Auth: no; optional Bearer of a recent login to link Steam to that account.
  • Body: {"ticket_hex":string, "identity":string} (the ticket hex-encoded, at most 8192 characters; the identity string the ticket was requested for).
  • 200: AuthSession.
  • Errors: 401 steam_auth_failed, or the Bearer token sent is token_expired / unauthorized; 403 banned, a refused borrowed copy, or reauthentication_required (linking needs a login younger than 10 minutes); 404 Steam login is not enabled on this server; 409 conflict (the Steam account is linked to another account, or this account already has one).

POST /v1/auth/refresh#

  • Auth: no.
  • Body: {"refresh_token":string}.
  • 200: TokenPair (a new pair; the old refresh token is used up).
  • Errors: 401 unauthorized (invalid, expired or revoked) or refresh_token_reused (the session was revoked); 403 banned.

POST /v1/auth/logout#

Revoke this session, or every session of the account.

  • Auth: the Bearer token or refresh_token in the body.
  • Body: {"everywhere"?:bool, "refresh_token"?:string} (send {} for "this session, by Bearer").
  • 200: {}.
  • Errors: 401 unauthorized (neither a valid access token nor a valid refresh token).

POST /v1/auth/email/verify#

  • Auth: no.
  • Body: {"token":string} (the one-time token from the mail).
  • 200: {}.
  • Errors: 400 invalid_token.

POST /v1/auth/email/resend#

Send the verification mail again (nothing happens when the address is confirmed already).

  • Auth: yes. No body.
  • 200: {}.
  • Errors: 401; 429 rate_limited.

POST /v1/auth/password/forgot#

  • Auth: no.
  • Body: {"email":string}.
  • 200: {}, always the same whether or not the address has an account (a mail is sent if it does).
  • Errors: 429 rate_limited.

POST /v1/auth/password/reset#

  • Auth: no.
  • Body: {"token":string, "new_password":string}.
  • 200: {}. Every session of the account is revoked.
  • Errors: 400 invalid_token; 422 validation_failed.

Account#

GET /v1/account#

  • Auth: yes.
  • 200: Account.

PATCH /v1/account#

Change the caller's account (absent fields stay).

  • Auth: yes.
  • Body: {"display_name"?:string}.
  • 200: the changed Account.
  • Errors: 422 validation_failed.

POST /v1/account/password#

Change the password, knowing the current one; the other sessions are revoked (this one stays).

  • Auth: yes.
  • Body: {"current_password":string, "new_password":string}.
  • 200: {}.
  • Errors: 401 invalid_credentials (the current password is wrong); 422 validation_failed.

DELETE /v1/account/identities/{provider}#

Unlink a login provider (steam) from the caller's account.

  • Auth: yes, with a login younger than 10 minutes.
  • 200: {}.
  • Errors: 403 reauthentication_required (log in again first); 404 not_found (no such linked provider); 409 conflict (it is the account's only way to log in).

Storage#

Every route addresses the caller's own objects. See Storage flows.

GET /v1/storage/{collection}#

A page of the caller's objects in a collection, without values, ordered by key.

  • Auth: yes.
  • Query: cursor?, limit? (1–100, default 50).
  • 200: Page<StorageObjectInfo>.
  • Errors: 400 bad_request (an invalid name or cursor).

GET /v1/storage/{collection}/{key}#

  • Auth: yes.
  • 200: StorageObject; header ETag: "<version>".
  • Errors: 404 not_found.

PUT /v1/storage/{collection}/{key}#

Write one object. Without a condition the last write wins.

  • Auth: yes.
  • Body: {"value":JSON, "if_version"?:int}. if_version: N writes only over version N; if_version: 0 writes only if the object does not exist.
  • Headers (alternative to if_version): If-Match: "N", If-None-Match: *.
  • 200: ObjectAck (with the new version); header ETag: "<version>".
  • Errors: 400 bad_request (an invalid name, a malformed condition header, or a header that disagrees with if_version); 403 forbidden (the object or its collection is written by the server only), quota_exceeded (too many objects or bytes), or a server rule's refusal; 409 version_conflict (details {"current_version":N}); 413 payload_too_large; 422 validation_failed (the value is too large); 429 rate_limited (details retry_after_ms).

DELETE /v1/storage/{collection}/{key}#

Deleting an object that does not exist answers {} too, unless a version is named.

  • Auth: yes.
  • Query: if_version? (only if the stored version is this one). Header alternative: If-Match: "N".
  • 200: {}.
  • Errors: 403 forbidden (server-locked) or a server rule's refusal; 409 version_conflict; 429 rate_limited.

POST /v1/storage/_batch/get#

Read several objects at once.

  • Auth: yes.
  • Body: {"objects":[{"collection":string, "key":string}]} (1–16 distinct objects).
  • 200: {"objects":[StorageObject]}; missing objects are simply absent.
  • Errors: 413 payload_too_large (more than 4 MiB of values: read fewer per batch); 422 validation_failed.

POST /v1/storage/_batch/put#

Write several objects in one transaction: all or nothing.

  • Auth: yes.
  • Body: {"objects":[{"collection":string, "key":string, "value":JSON, "if_version"?:int}]} (1–16 distinct objects, at most 4 MiB of values together).
  • 200: {"objects":[ObjectAck]}, in request order.
  • Errors: 403 forbidden / quota_exceeded / a server rule's refusal (details: index of the failing item); 409 version_conflict (details: index and current_version of the first failing item); 422 validation_failed; 429 rate_limited (a batch counts as one write).

Chat#

Joining, sending and live messages use the WebSocket; these HTTP routes list rooms, read history, open direct messages and delete messages. See Chat flows.

GET /v1/chat/rooms#

The public rooms, with online counts and caps.

  • Auth: yes.
  • Query: cursor?, limit?.
  • 200: Page<RoomInfo>.

GET /v1/chat/rooms/{room}/messages#

A page of a room's history, newest first.

  • Auth: yes. Public rooms: any player; group and DM rooms: their members.
  • Query: cursor? (older messages), limit?.
  • 200: Page<ChatMessage>.
  • Errors: 403 not_a_member; 404 not_found.

DELETE /v1/chat/rooms/{room}/messages/{message}#

Delete a message: its sender, or a moderator (roles admin / moderator by default). The room gets a chat.deleted push and the message leaves the history.

  • Auth: yes.
  • 200: {}.
  • Errors: 403 forbidden (neither its sender nor a moderator); 404 not_found (no such message, or deleted already).

POST /v1/chat/dm#

Open (or find) the direct-message room with another user.

  • Auth: yes.
  • Body: {"user":int}.
  • 200: RoomInfo (kind: "dm", with peer).
  • Errors: 400 bad_request (yourself); 403 a server rule's refusal (for example a block); 404 not_found (no such account); 429 rate_limited (details retry_after_ms).

GET /v1/chat/dms#

The caller's direct-message rooms, newest activity first.

  • Auth: yes.
  • Query: cursor?, limit?.
  • 200: Page<RoomInfo> (each with peer).

Admin (role admin only)# admin only

Every admin route needs the Bearer token of an account with the admin role; others get 403 forbidden. Every admin action is recorded in the audit log. These routes are for operator tools, never for player-facing pages.

text
AdminUser   {"account":Account, "active_sessions":int, "last_seen_at"?:ms, "ban"?:BanInfo}
BanInfo     {"banned_at":ms, "until"?:ms, "reason"?:string}
AuditEntry  {"id":int, "action":string, "actor"?:int, "target_type"?:string, "target_id"?:string,
             "ip"?:string, "request_id"?:string, "data"?:object, "created_at":ms}
Method Path Request Answer Errors
GET /v1/admin/users query q? (part of the email, case-insensitive, or display name, or an id), cursor?, limit? Page<AdminUser>, newest first
GET /v1/admin/users/{user} – AdminUser 404
POST /v1/admin/users/{user}/ban {"until"?:ms (in the future; absent = until lifted), "reason"?:string (≤ 255 characters)} {}; sessions revoked, sockets closed with 4003 404; 409 conflict (yourself, or an admin: remove the role first); 422
POST /v1/admin/users/{user}/unban – {} 404
DELETE /v1/admin/users/{user}/sessions – {}; every session revoked 404
DELETE /v1/admin/users/{user}/identities/{provider} – {} (unlink, e.g. steam) 404
PUT /v1/admin/users/{user}/roles/{role} – {} (granted, or already held) 404
DELETE /v1/admin/users/{user}/roles/{role} – {} (revoked, or not held) 409 conflict (your own admin role, or the last admin)
GET /v1/admin/audit query user? (acted or was the target), action? (one action, or a prefix ending in . such as admin.), cursor?, limit? Page<AuditEntry>, newest first
GET /v1/admin/users/{user}/storage/{collection} query cursor?, limit? Page<StorageObjectInfo>
GET /v1/admin/users/{user}/storage/{collection}/{key} – StorageObject 404
PUT /v1/admin/users/{user}/storage/{collection}/{key} {"value":JSON, "if_version"?:int, "write"?:"owner"\|"server"} (write absent: unchanged; new objects owner) ObjectAck 404 (no such account); 409 version_conflict; 422
DELETE /v1/admin/users/{user}/storage/{collection}/{key} query if_version? {} (also for a server-locked object) 409 version_conflict

An admin storage write ignores the owner's write lock and the owner's quotas.

Generated from API.md (repository commit e25edb9, file sha256 3db87f7bba70).