net_backend

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.

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.

the envelope
{"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.

a game rule as a hook
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

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 →