net_backend

Get started · from zero to a running server

Build your server.

net_backend_server is a library you build your own server binary with. A few steps: add it, write main.rs, run it, connect a client. Or start from the ready-to-run reference server.

1. Add the crate

MySQL is the default database. For a first run, SQLite needs no database server at all.

Cargo.toml
[dependencies]
net_backend_server = { version = "0.1.0" }                                   # MySQL
# net_backend_server = { version = "0.1.0", default-features = false, features = ["sqlite"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
serde = { version = "1", features = ["derive"] }

The backends are additive (mysql, postgres, sqlite) and the modules are features too (storage, chat, plus steam and smtp): a server compiles only what it registers. The framework re-exports axum, sea_query, sqlx, utoipa, utoipa_axum and the protocol crate. Rust 1.95 or newer.

2. Write main.rs

Your game's routes are plain axum handlers. .run() gives the server its command line, with serve as the default.

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
}

3. Run it

shell
NBS__DATABASE__URL=sqlite::memory: cargo run       # with the `sqlite` feature
curl http://127.0.0.1:8080/v1/info                 # {"protocol":1,"min_protocol":1,"modules":[]}

Configuration comes from defaults, then a TOML file (the path in NBS_CONFIG, else ./config.toml), then NBS__SECTION__KEY environment variables, then secrets read from files. Unknown keys are errors, so a typo never silently falls back to a default.

config.toml
[server]
bind = "127.0.0.1:8080"        # behind a reverse proxy such as Caddy

[database]
url = "mysql://game:secret@127.0.0.1:3306/game"   # or url_file = "/run/credentials/game/db_url"

[ws]                           # the WebSocket hub at /v1/ws
enabled = true
max_connections = 10000        # 503 above

[modules.auth]                 # each module reads its own section
app_name = "My Game"           # in mail subjects

4. Add accounts, saves and chat

The built-in modules register like any other. This is how the reference server that the deployment files install is put together:

src/main.rs
use net_backend_server::chat::Chat;
use net_backend_server::storage::Storage;
use net_backend_server::{Auth, Config, NetBackendServer};

#[tokio::main]
async fn main() -> Result<(), net_backend_server::Error> {
    NetBackendServer::new(Config::load()?)
        .module(Auth::new())       // accounts, logins, tokens, roles: [modules.auth]
        .module(Storage::new())    // per-player saves (feature `storage`)
        .module(Chat::new())       // rooms, DMs, presence (feature `chat`)
        .run()
        .await
}

Your rules go in hooks: a before hook can pass an event on, change it or reject it, so the game decides what a valid save or a valid name is. See modules and hooks.

5. Connect a client

Pick the path that matches your client. Every path talks to the same server.

Your client is…Use
a Rust app (tool, bot, CLI, other engine)net_backend_client + net_backend_protocol
a Bevy gamebevy_net_backend + net_backend_protocol
other Rust codenet_backend_protocol + any HTTP / WebSocket library (reqwest, ureq, tokio-tungstenite, …)
not Rust (C#, GDScript, JavaScript, …)the API directly: the API reference, /v1/openapi.json and /v1/asyncapi.json

Try the Rust client against the reference server from the top of this page: examples/quickstart.rs registers (or logs in), reads the account, saves and loads a storage object, and logs out.

shell · a second terminal
NET_BACKEND_URL=http://127.0.0.1:8080 NET_BACKEND_EMAIL=player@example.com NET_BACKEND_PASSWORD="a long password" \
    cargo run -p net_backend_client --example quickstart
# server speaks protocol 1..=1, modules ["auth", "chat", "storage"]
# registered
# account 1 (Some("player@example.com"))
# saved version 1; loaded {"level":3}
# logged out

More about the client (the session, typed errors, the WebSocket, SSH): net_backend_client →

6. Deploy

The repository's deploy/ folder runs a server on one Linux machine with Docker Compose or systemd, Caddy for HTTPS + WSS, migrations on every deploy and daily backups. Your own server binary has the same command line, so the same files work for it. Deploying a server →