net_backend
Rust crates · 0.1.0 · MIT OR Apache-2.0

Build your own game backend in Rust: server, protocol and client.

A framework, not a hosted service: you build your own server binary with it, and your game rules stay in your code, as plain Rust. It speaks plain HTTP + WebSocket + JSON, so any client in any language can use it.

accounts & authWebSocket hubsaveschatself-hosted
~/my_game_server
# your server, built with net_backend_server
$ cargo run
# any client, any language
$ curl -sS "$API/v1/info"
{"protocol":1,"min_protocol":1,"modules":["auth","chat","storage"]}

01 What is your client?

One server. Four ways to talk to it.

You choose how much of net_backend you use. Every path uses the same server, and the server does not care which client connects: the JSON on the wire is the contract.

Server + protocol + net_backend_client

for Rust apps, tools, bots and other Rust engines

Typed calls for every server route, the session handled for you (the access token is refreshed before it expires, one refresh shared by all callers), an async API on tokio and a blocking interface for game loops. WebSocket, SSH and SFTP are opt-in features.

net_backend_client
use net_backend_client::protocol::auth::{GetAccount, LoginRequest};
use net_backend_client::protocol::storage::{GetObject, PutObject, WriteObject};
use net_backend_client::{Client, Error};

let client = Client::new("https://api.example.com")?;
client.login(LoginRequest::new("player@example.com", "a long password")).await?;

let me = client.call(&GetAccount::new()).await?;
client.call(&WriteObject::new("saves", "slot-1", PutObject::new(serde_json::json!({"level": 3})))).await?;
let save = client.call(&GetObject::new("saves", "slot-1")).await?;
println!("{:?} is on level {}", me.display_name, save.value["level"]);

every path → the same net_backend_server

02 How it fits together

Your rules on top. A shared protocol underneath.

net_backend_server is a library you build your own server binary with. The protocol crate holds the message types both sides speak; clients in other languages send the same JSON.

net_backend architecture Your client (Rust with net_backend_client, Bevy with bevy_net_backend, any HTTP or WebSocket library, or the API directly) talks HTTP and WebSocket to your server built on net_backend_server. Both sides share net_backend_protocol. your client your game / app / tool · Rust: net_backend_client · Bevy: bevy_net_backend · Other: any HTTP / WS library · Not Rust: the API (JSON) HTTP + WebSocket client your server (Rust binary) your rules / hooks / logic net_backend_server core: auth, sessions, WS hub modules: storage, chat MySQL · PostgreSQL · SQLite HTTP / WS JSON answers + pushes net_backend_protocol shared message types · plain Rust + serde · no networking, no async runtime, no engine
Not Rust? Use the HTTP / WebSocket API directly: the API reference → Browser games on another domain work too, once the server lists their origin in cors.allowed_origins.

03 In the box

The parts every online game ends up building.

Solid building blocks with sensible defaults. Modules are Cargo features: a server compiles only what it registers.

auth

Accounts & auth

Email + password (argon2id) and Steam logins, rotating refresh tokens, sessions and revocation, email verification, password reset, roles, an audit log and admin routes.

/v1/ws

WebSocket hub

One JSON envelope: handlers by kind, pushes to a socket, user, room or everyone, rooms with caps, heartbeats, per-socket rate limits and bounded outboxes.

storage

Saves

Per-player JSON objects with versions and conditional writes (if_version, If-Match, ETag), batches, quotas and audited admin access.

chat

Chat

Public, group and DM rooms, history pages, presence, caps and rates, moderation hooks and deletion, retention.

MySQL · PostgreSQL · SQLite

One Db handle for all three; statements built once with sea-query. The backends are additive features.

Migrations

Plain SQL per dialect: ordered, tracked, checksummed and safe against concurrent runs. Publish a module's migrations into your app to own them.

Modules & hooks

Routes, migrations and hooks under one name. Typed before, in_tx and after hooks with time limits; panics are contained.

OpenAPI + AsyncAPI

/v1/openapi.json for every HTTP route and /v1/asyncapi.json (AsyncAPI 3.0) for the WebSocket, generated from what you register.

Rate limits

HTTP rate limits and a failed-login lockout; WebSocket handshake, connection, frame-rate and size limits.

Configuration

A TOML file plus NBS__SECTION__KEY overrides and secrets from files; validated, every problem reported at once.

Metrics

Optional Prometheus metrics on their own loopback listener: HTTP requests and durations, WebSocket connections, frames and closes.

Graceful shutdown

On SIGTERM: stop accepting, /readyz answers 503, sockets get 1001, in-flight work gets a grace period, modules shut down in reverse order.

All features in detail →

04 Your server

A server is a few lines of Rust.

Plain axum handlers for your game's routes, modules for what you need, and a command line you get for free.

  • No scripting runtime: your rules are Rust code.
  • serve, migrate, config check, openapi export and more from .run().
  • Every 4xx / 5xx is the protocol's error body; internal errors never reach a client.

Build and run it →

src/main.rs
use net_backend_server::axum::{routing::post, Json};
use net_backend_server::{ApiJson, AppError, Config, NetBackendServer};
use serde::Deserialize;

#[derive(Deserialize)]
struct Craft {
    item: String,
}

async fn craft(ApiJson(body): ApiJson<Craft>) -> Result<Json<String>, AppError> {
    if body.item.is_empty() {
        return Err(AppError::bad_request("item is empty"));
    }
    Ok(Json(format!("crafted {}", body.item)))
}

#[tokio::main]
async fn main() -> Result<(), net_backend_server::Error> {
    let config = Config::load()?;                    // NBS_CONFIG / config.toml + NBS__* variables
    NetBackendServer::new(config)
        .route("/v1/game/craft", post(craft))      // plain axum handlers
        .run()                                     // the command line; `serve` by default
        .await
}

05 Self-hosted

Your machine. Your data.

Run it on your own machine or VPS with Docker Compose or systemd: HTTPS and WSS through Caddy, migrations on every deploy, daily backups with a restore that checks the backup before it changes anything, and an SSH hardening guide.

Measured on a small VPS

10 000authenticated WebSockets over HTTPS through Caddy: all connected, none closed during the hold; server ~15 KiB per socket
600 000of 600 000 chat deliveries over HTTPS: 200 members, 50 senders at 1 message/s, every message stored
~4 500/sHTTP requests on /v1/info through Caddy, 64 connections
5000of 5000 sockets reconnected after a restart closed them with 1001

Measured on a 2 vCPU / 7.8 GiB virtual machine (Ubuntu 24.04, MySQL 8 or PostgreSQL 16 on the same machine, Caddy in front). The HTTPS rows ran load_test on that same machine, so it shared the 2 vCPU with the server, Caddy and the database. Every number and its setup →

Deploying a server →

06 Free and open source

MIT OR Apache-2.0. Build on it.

One Cargo workspace: the server, the protocol and the Rust client, each with its own version, changelog and README. Issues and pull requests are welcome.