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.
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.
# 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?
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.
net_backend_clientfor 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.
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"]);
bevy_net_backendfor Bevy games
The Bevy plugin: a system fires a request and gets a RequestId back at once; a few frames later exactly one typed answer arrives as a Bevy message. HTTP by default, WebSocket and SSH / SFTP as features.
fn submit_score(backend: Res<HttpClient>, mut commands: Commands) {
let id = backend.post_json::<Rank>("/scores", &Score { level: 3, points: 12_500 });
commands.insert_resource(Submitting(id));
}
App::new()
.add_plugins((MinimalPlugins, BackendPlugin::new(HttpConfig::new("https://api.example.com/v1"))))
.add_json_response::<Rank>()
for Rust developers who want their own networking
The shared message types plus any HTTP / WebSocket library (reqwest, ureq, tokio-tungstenite, …). Both sides import the same types, so a mismatch is a compile error instead of a runtime surprise.
use net_backend_protocol::chat::SendMessage;
use net_backend_protocol::{RoomId, WsAuth, WsRequestFrame};
// First message on a fresh socket (if the handshake carried no `Authorization` header).
let auth = WsAuth::new("the-access-token-from-login").to_message();
// A request: {"id":1,"type":"chat.send","data":{"room":12,"text":"hello"}}
let request = WsRequestFrame::call(1, SendMessage::new(RoomId(12), "hello"));
let text = serde_json::to_string(&request).unwrap();
for web pages, JavaScript, C#, Godot / GDScript, Unity, anything
No Rust needed on the client: HTTP + WebSocket + JSON, documented in the API reference, with an OpenAPI document for HTTP and an AsyncAPI document for the WebSocket on every running server.
# Server info (no auth)
curl -sS "$API/v1/info"
# {"protocol":1,"min_protocol":1,"modules":["auth","chat","storage"]}
# Log in
curl -sS "$API/v1/auth/login" -H 'content-type: application/json' \
-d '{"email":"player@example.com","password":"correct horse battery"}'
every path → the same net_backend_server
02 How it fits together
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.
cors.allowed_origins.03 In the box
Solid building blocks with sensible defaults. Modules are Cargo features: a server compiles only what it registers.
Email + password (argon2id) and Steam logins, rotating refresh tokens, sessions and revocation, email verification, password reset, roles, an audit log and admin routes.
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.
Per-player JSON objects with versions and conditional writes (if_version, If-Match, ETag), batches, quotas and audited admin access.
Public, group and DM rooms, history pages, presence, caps and rates, moderation hooks and deletion, retention.
One Db handle for all three; statements built once with sea-query. The backends are additive features.
Plain SQL per dialect: ordered, tracked, checksummed and safe against concurrent runs. Publish a module's migrations into your app to own them.
Routes, migrations and hooks under one name. Typed before, in_tx and after hooks with time limits; panics are contained.
/v1/openapi.json for every HTTP route and /v1/asyncapi.json (AsyncAPI 3.0) for the WebSocket, generated from what you register.
HTTP rate limits and a failed-login lockout; WebSocket handshake, connection, frame-rate and size limits.
A TOML file plus NBS__SECTION__KEY overrides and secrets from files; validated, every problem reported at once.
Optional Prometheus metrics on their own loopback listener: HTTP requests and durations, WebSocket connections, frames and closes.
On SIGTERM: stop accepting, /readyz answers 503, sockets get 1001, in-flight work gets a grace period, modules shut down in reverse order.
04 Your server
Plain axum handlers for your game's routes, modules for what you need, and a command line you get for free.
serve, migrate, config check, openapi export and more from .run().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
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
/v1/info through Caddy, 64 connectionsMeasured 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 →
06 Free and open source
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.