Features · net_backend_server 0.1.0
Everything in the box.
A Rust framework for game backend servers: async (tokio + axum) and modular. It gives you solid building blocks with sensible defaults and leaves the game rules to you.
Accounts and authentication
The built-in Auth module: email + password (argon2id) and Steam logins, opaque access and rotating refresh tokens stored as hashes, sessions and revocation, email verification and password reset (log or SMTP mailer), roles, an audit log, /v1/admin routes, rate limits and a failed-login lockout, hooks, and user:* commands.
- Access tokens live 1 hour and refresh tokens 30 days by default; every refresh token works once and is replaced by a new pair.
- Logout revokes one session or all of them, with the access token or, once it has expired, the refresh token; open WebSockets of a revoked session are closed.
- Roles are strings (
admin,moderator, your own); the admin routes needadmin, and every admin action is in the audit log.
WebSocket hub
/v1/ws with the protocol's envelope: auth at the handshake (Bearer) or by first message, request handlers by kind (typed or JSON) for the game and modules, pushes to a socket, user, room or everyone, rooms with caps, close on revocation (4001) and ban (4003), connection caps, per-socket rate limits and bounded outboxes, heartbeats, hooks, 1001 on shutdown, and a pub/sub seam for several instances.
{"id":7,"type":"chat.send","data":{"room":12,"text":"hello"}} // request
{"id":7,"ok":true,"data":{"message_id":981,"sent_at":1790000000000}} // its one answer
{"type":"chat.message","data":{"id":981,"room":12,"sender":42}} // a push
Every request gets exactly one answer. The full rules, close codes and registered kinds are in the WebSocket reference.
Storage (saves)
The Storage module (feature storage): per-user JSON objects with versions, conditional writes (if_version, If-Match, ETag), batches, a server write lock, quotas, hooks (including inside the transaction), and audited admin access. Two devices writing the same save cannot silently overwrite each other: a stale version answers 409 version_conflict.
Chat
The Chat module (feature chat): public, group and DM rooms, the answer before the echo, history pages, caps and rates, moderation hooks and deletion, presence with a cap and a rate, and retention. Server code creates rooms and groups.
Databases
MySQL (default), PostgreSQL and SQLite through one Db handle; statements built once with sea-query for all three, and portable column types. The CI database job runs MySQL 8.4, MariaDB 11.4 and PostgreSQL 16. TLS uses rustls + ring: no OpenSSL, no aws-lc.
Migrations
Plain SQL per dialect: ordered, tracked and checksummed, namespaced per module, safe against concurrent runs on every backend, with precise recovery messages. A module's migrations can be published into your app, which then owns them. Plain SQL means you can read, review and change them, per database.
Modules and hooks
A module bundles routes, migrations, hooks and OpenAPI parts under a name. Only name is required.
- Order is deterministic: registration order for migrations, routes and
start;shutdownruns in reverse. - Hooks:
beforehooks may pass the event on, modify it or reject it;in_txhooks run inside a module's transaction;afterhooks run after the work. Each call has a time limit, and a panic in a hook is caught: the server keeps running. - Dependencies: a module names the modules it needs (
depends_on); each must be registered before it.
NetBackendServer::new(config)
.module(Scores)
.before::<BeforeSubmit, _, _>(|_ctx, mut event| async move {
event.points = event.points.min(1_000_000); // the game's own rule
Ok(Decision::Continue(event))
})
.run()
.await
Typed routes
Every HTTP route of the protocol is mounted from its HttpCall (method, path, payload, answer); the same works for your own routes (.call::<C, ..>(handler), call_route!). Rust clients use the same types, so both sides agree at compile time.
OpenAPI and AsyncAPI
GET /v1/openapi.json describes the core routes, every module's documented routes and yours (utoipa), with an optional browser UI. GET /v1/asyncapi.json is the AsyncAPI 3.0 document of the WebSocket endpoint, generated from the registered kinds. Generate typed clients from them.
Rate limits and sizes
HTTP rate limits and a failed-login lockout; body limits (64 KiB default, 32 MiB hard cap); a header-read timeout and request timeouts. On the WebSocket: handshakes per address, connections per user and per address, frames per second with a burst, and a message size limit.
Configuration and operations
- Configuration: a TOML file plus
NBS__SECTION__KEYoverrides and secrets from files; typed and validated, every problem reported at once, secrets never inDebug. - Command line:
serve,migrate,migrations publish,config check,openapi export,asyncapi export. - Health:
/healthzand/readyz(checks the database with a 1 s limit). - Metrics: optional Prometheus text on its own loopback listener, never on the API port.
- Graceful shutdown: on SIGTERM or Ctrl-C the server stops accepting connections,
/readyzanswers 503, every WebSocket gets close 1001, and in-flight work getsserver.shutdown_grace_secsto finish.
Any client
The server never links Bevy and never assumes a client library: it speaks plain HTTP + WebSocket + JSON. The four ways to use it →
Browser games on another domain use the API fully once the operator lists their origin in cors.allowed_origins: the request headers include If-Match and If-None-Match, and the page can read ETag and Retry-After. A page served from the API's own origin needs no CORS, and WebSockets are not subject to it. CORS in the API reference →