Getting Started

1

Introduction

What is Jondob Multiplayer Backend?

Jondob Multiplayer Backend (JMB) is a complete multiplayer backend built in Go. It is highly performant and engine-agnostic, and it comes with direct integration for Godot 4. You have access to source code, and it is one Go server binary you run yourself.

Out of the box you get account management (Steam, email/password, and more login options to be added in the future), lobbies with a full match-start flow, chat and direct messages, friends, blocking, synced player data, world data, generic data, and authoritative game-server orchestration in two modes: world and matches.

The server runs on any machine you control — a VPS, your own PC, whatever you want. Everything is stored on that machine, and everything is completely configurable by editing a text file.

This guide takes you from an empty project to a running multiplayer game: SDK installed, server configured, builds exported, and your first match running.

2

Godot Setup

Install JMB in Godot

Configure your project

Key configuration files

Go to the addons/01_jg_lib/packs/jmb/ directory, as shown in the screenshot:

The Godot FileSystem dock showing the packs/jmb directory with build_config.tres and client_config.tres

Then duplicate build_config.tres and client_config.tres, and place them in a directory of your choosing. Ideally, create your own directory that keeps everything related to your game in one place.

FilePurpose
build_config.tresA helper resource you can use to build your game quickly using specific configuration (scenes, export presets, output directories).
client_config.tresThe main configuration file that controls how the Godot client connects to the Go server. Typically, you host your Go server on a VPS (such as DigitalOcean or Linode), and this is where you set the connection details.

Core scenes configuration

jmb_service.tscn is the key file where all the core logic lives. You will only need to interact with it during the setup process, and you only need to change one thing:

jmb_network_manager.tscn is the file you will interact with most of the time. You can use it as-is, or follow the recommended flow:

client_config.tres in the Godot inspector, showing Core Settings, TLS Settings, and Connection Settings

Finally, set the new service scene you created as an autoload: Project → Project Settings → Globals → Autoload.

Tip
The core is now ready in Godot. The autoloaded service scene carries the user manager, REST and WebSocket clients, and every high-level adapter (login, lobby, chat, friends). Anywhere in your code, JMB_ServiceController.get_instance() gives you the running service.

Creating core scenes

You need to set up two scenes in the network manager to act as entry and exit points for online gameplay:

ScenePurpose
Online SceneThe first scene that runs when the player is networked. This is where your match or world begins. Once you reach this scene, the player is online in terms of gameplay.
Offline SceneThe scene all clients return to when the game is over — whether they disconnect from a world or from a match.
i
Note
In the demo directory, sample_gameplay and sample_server_intro contain sample scenes that you can reference and assign through the network manager.
3

Build & Export

JMB uses a single Godot project for both client and server. You export two builds from the same project using export presets.

Export presets

Make sure you have export templates installed in Godot (Editor → Manage Export Templates). Then set up your export presets in Project → Export.

Build using JMB_BuildConfig

Select your JMB_BuildConfig resource and configure it to point to the scenes, output directory, and export preset name.

Export presets are the presets you define in the Export window. The build system uses these for creating builds.

!
Preset naming
Pay attention to the preset Name field. It must match the Client Preset Name and Server Preset Name in your build config exactly. You do not have to use the same preset for both exports, but for a quick start you can point both to the same one.
4

Server Setup & Run

Install Go

JMB's server is written in Go. Install Go 1.25 or later from go.dev/doc/install.

Verify the installation:

go version

If it prints version 1.25 or above, you are ready to proceed.

Configure the Go server

In the Go server source code, the only directory you need to work with is config/. It contains everything you can configure.

Server config directory tree showing auth, game_data, game_insts, lobby, networking, templates, config.json, database.json, endpoints.json, and gameinst_definitions.json

config.json

Start with config.json, which defines the server's listen addresses:

{
  "public": {
    "address": "0.0.0.0:8080"
  },
  "internal": {
    "address": "127.0.0.1:25001"
  }
}

If you look at client_config.tres in Godot, you will find that these values match. They must be exactly equal, or the client will not be able to connect.

endpoints.json

Configure which login endpoints are available:

{
  "login_with_email": {
    "enabled": true,
    "confirmed": true
  },
  "login_with_id_t": {
    "enabled": true,
    "confirmed": true
  },
  "login_with_steam": {
    "enabled": false,
    "confirmed": true
  }
}

To support a platform, set its enabled to true. The confirmed field is an extra safety measure — if it is false, the server will log a warning about that endpoint.

i
Debug login
login_with_id_t is a special endpoint with no protection — anyone can enter any string and log in with it. No password, no validation. It is intended for debugging and development builds, so you do not need to create accounts during playtesting.

gameinst_definitions.json

This file tells the Go server where your game executable lives, so it knows how to launch the Godot server build:

{
  "all_game_entries": {
    "gk_main_world_entry": {
      "executable": "C:/path/to/your/GameServer.exe",
      "display_name": "Sample Game"
    }
  },
  "port_range_start": 9000,
  "port_range_end": 19000
}

Set the executable field to the absolute path of your exported Godot server build on your operating system.

Run the Go server

Open a terminal in the directory that contains the Go server source, then run:

go build .
go run .

That's it. If you see the following output, the server is running:

INFO  lobby: loaded 8 lobby var(s), 1 member var(s)
INFO  backup: routine started
INFO  ws router started
INFO  ws hub started
INFO  lobby started
INFO  JMB :: Jondob Multiplayer Backend
INFO  internal listener running on 127.0.0.1:25001
INFO  public listener running on 0.0.0.0:8080

Run the game

The server is running and everything is configured. Launch the client, and you are live.

JMB sample game main menu with Single Player and Multiplayer buttons

The premade login UI supports Steam, email/password, and debug token login out of the box:

JMB login screen showing Sign in through Steam, Continue with Email, and Continue as Guest

Create an account, log in, and enter the lobby:

JMB account creation form with username, email, password, and confirm password fields

The room browser shows available rooms, a chat panel, and online players:

JMB lobby room browser with chat messages and online players list

Inside a room, you can see the player list, room settings, room chat, and the Start Match button:

Inside a JMB lobby room showing players, room settings, room chat, and Start Match button
Godot terminal output showing JMB connected with backup logs
Tip
The premade UI for login, lobbies, chat, and friends is fully functional out of the box. You can restyle it to match your game, or build your own UI on top of the adapters and signals described in the API reference.
5

Game Data

JMB supports the core plain data types: bool, string, float. Whether you want to give players XP, track lifetime kills across all matches, or check whether a monument in a persistent world has been destroyed — it all works through GenVar config files.

Data configuration files

Modify one of these files depending on your needs, by adding or removing variables:

game_data directory listing showing all GenVar config files
FilePurpose
bucket_bvars.jsonVariables not tied to any game world or player. Useful for async multiplayer where only data moves around, or for shared group data like guild stats.
lobby_member_vars.jsonData that players carry alongside themselves from the lobby into the match — for example, player color, faction, or team.
lobby_vars.jsonVariables relevant to a lobby room: the active map, max players, game mode, and so on.
player_qvars.jsonPersistent player data: XP, level, coins, tokens, unlocks, and any per-player progress.
world_wvars.jsonData local to a persistent world — world state, event flags, and server-wide toggles that belong to that world and no one else.

Persistent worlds

If your game is world-based or uses a persistent world that runs forever and players can join and leave freely, open persistent_worlds.json:

game_insts directory with persistent_worlds.json highlighted
{
  "world_spawn_interval_seconds": 2,
  "worlds": [
    {
      "game_key": "gk_main_world_entry",
      "game_id": "world_main",
      "display_name": "Main World",
      "enabled": false,
      "max_players": 64
    }
  ]
}

The key variables you need to modify:

6

Storage & Backups

Database

JMB supports both SQLite and Postgres. By default it uses SQLite — the database is a single file stored at data/jmb.db.

Server directory tree showing data/jmb.db, jmb.db-shm, and jmb.db-wal files

You can change the database name, save location, or switch between Postgres and SQLite through database.json:

Config directory with database.json highlighted
{
  "target_db": "sqlite",
  "entries": [
    {
      "driver_name": "sqlite",
      "connection_string": "file:data/jmb.db?_pragma=..."
    },
    {
      "driver_name": "postgres",
      "connection_string": "..."
    }
  ]
}

To rename your database, modify the connection string — for example, changing data/jmb.db to data/mySuperCoolGame.db will create a database with that name instead.

Automatic backups

If you use SQLite, backing up is straightforward: the entire database is a single file. You can copy jmb.db anywhere you want as a manual backup. JMB also provides an automatic backup system.

Backups directory showing backup_01.db, backup_02.db, backup_03.db

Configure automatic backups in the database.json file:

"can_use_automatic_backups": true,
"backup_interval_minutes": 1440,
"max_backups": 10,
"backup_dir": "backups"
FieldMeaning
can_use_automatic_backupsEnable or disable the automatic backup routine.
backup_interval_minutesHow often a backup is taken (default: every 24 hours).
max_backupsMaximum number of backup files to keep. Oldest are rotated out.
backup_dirDirectory where backups are stored.

Backups are named in ascending order: backup_01 is always the oldest. For Postgres, backups are stored as .sql files instead of .db files.

Stuck? Say hello
Questions, feedback, or a setup that won't behave — the fastest route is the Jondob Games Discord.
7

Calling the Go API

The JMB SDK talks to the Go server through two channels: a REST client for one-off HTTP requests and a WebSocket client for real-time, persistent communication. Both are accessed through JMB_ServiceController, the singleton that holds all core service references.

Getting the service reference

Everything starts with JMB_ServiceController.get_instance(). From the returned service you can reach the REST client, WS client, and user manager.

var service: JMB_ServiceController = JMB_ServiceController.get_instance()
var rest_client: JMB_RESTClient = service.get_rest_client()
var ws_client: JMB_WSClient = service.get_ws_client()
var user_manager: JMB_UserManager = service.get_user_manager()

Logging in

Login is handled by JMB_UserManager. Call login() with a username or email and a password. The result comes back through the on_login_completed signal.

var service: JMB_ServiceController = JMB_ServiceController.get_instance()
var user_manager: JMB_UserManager = service.get_user_manager()

user_manager.on_login_completed.connect(_on_login_completed)
user_manager.login("player@example.com", "my_password")

func _on_login_completed(result: JMB_RequestResult) -> void:
	if result.status == JMB_RequestResult.Status.OK:
		print("Logged in successfully")
	else:
		print("Login failed")
i
Other login methods
JMB_UserManager also provides login_with_steam(ticket) and login_with_id_t(external_id) for Steam and external-ID-token login. Registration uses register(username, password, email) and emits on_register_completed.

Logging out

Call logout() on the user manager. It sends a logout request to the server, clears local credentials, and emits on_logout_completed. After logout you typically return to the main menu.

var user_manager: JMB_UserManager = JMB_ServiceController.get_instance().get_user_manager()
user_manager.logout()
get_tree().change_scene_to_file("res://scenes/main_menu.tscn")

Using the REST client

The REST client handles all HTTP calls to the Go server. Every method comes in two forms: a callback version and an async version that you can await.

var rest_client: JMB_RESTClient = JMB_ServiceController.get_instance().get_rest_client()

# Check server health (async)
var health_result: JMB_RequestResult = await rest_client.check_health_async()
print("Server is reachable: ", health_result.status == JMB_RequestResult.Status.OK)

# Read player variables (async)
var keys: Array[String] = ["score", "level"]
var read_result: JMB_RequestResult = await rest_client.read_qvars_async(account_id, keys)
Async vs callback
Every REST method has a callback variant (e.g. check_health(callback)) and an async variant (e.g. check_health_async()). The async versions return JMB_RequestResult and are easier to use with await.

Using the WS client

The WebSocket client maintains a persistent connection to the server. It handles lobby rooms, chat, match events, and any custom real-time messaging. You send typed envelopes and receive responses through signals or await.

var ws_client: JMB_WSClient = JMB_ServiceController.get_instance().get_ws_client()

# Listen for connection events
ws_client.on_ws_opened.connect(_on_connected)
ws_client.on_ws_closed.connect(_on_disconnected)

# Subscribe to a message category
await ws_client.subscribe_to_category_async("lobby")

# Send a custom envelope and await the response
var envelope: JMB_WSOutEnvelope = JMB_WSOutEnvelope.new()
var response: JMB_WSInEnvelope = await ws_client.send_envelope_async(envelope)
i
Adapters handle most WS calls
In practice, you rarely call the WS client directly. The SDK provides adapters — JMB_LobbyAdapter, JMB_LobbyRoomAdapter, JMB_ChatAdapter — that wrap envelopes and manage state for you. Use the WS client directly only for custom server-side message types.
8

Quick Reference

A summary of the most important calls available through the REST client and the WebSocket adapters.

REST client calls

All methods below are on JMB_RESTClient. Each has a callback and an _async variant. Only the async signatures are listed.

Auth & account

MethodDescription
check_health_async()Ping the server to verify it is reachable.
register_async(username, password, email)Create a new account and auto-login.
login_async(identifier, password)Log in with email or username.
login_with_id_t_async(external_id)Log in with an external ID token.
login_with_steam_async(ticket)Log in with a Steam auth ticket.
logout_async()Invalidate the session on the server.
refresh_async()Refresh the auth token.
who_am_i_async()Return the current session's account info.
change_password_async(current, new)Change the account password.
change_email_async(current_password, new_email)Change the account email.

Profile

MethodDescription
get_my_profile_async()Fetch the logged-in player's profile.
update_profile_async(display_name)Update the display name.
get_other_profile_async(account_id)Fetch another player's profile by account ID.
find_player_by_friendcode_async(code)Look up a player by friend code.
regenerate_friendcode_async()Generate a new friend code for the current account.

Player, world & bucket variables

MethodDescription
read_qvars_async(account_id, keys)Read player variables (QVars) by key list.
write_qvars_async(account_id, vars)Write player variables.
read_wvars_async(world_id, keys)Read world variables (WVars) by key list.
write_wvars_async(world_id, vars)Write world variables.
read_bvars_async(bucket_id, keys)Read bucket variables (BVars) by key list.
write_bvars_async(bucket_id, vars)Write bucket variables.

Friends & blocking

MethodDescription
send_friend_request_async(account_id)Send a friend request to another player.
accept_friend_request_async(account_id)Accept a pending friend request.
cancel_friend_request_async(account_id)Cancel a sent or decline a received friend request.
remove_friend_async(account_id)Remove someone from the friends list.
block_player_async(account_id)Block a player.
unblock_player_async(account_id)Unblock a player.
get_friends_list_async()Fetch the full friends list.
get_pending_friend_requests_async()Fetch pending incoming/outgoing requests.
get_blocked_players_async()Fetch the blocked players list.

Leaderboards & chat

MethodDescription
get_lb_entries_async(var_key, limit, offset, sort)Fetch a page of leaderboard entries.
get_lb_around_async(var_key, account_id, count, sort)Fetch leaderboard entries around a specific player.
get_lb_position_async(var_key, account_id, sort)Get a player's position on a leaderboard.
get_chat_rooms_async()List available chat rooms.

WebSocket adapter calls

These methods live on the adapter classes, not on JMB_WSClient directly. The adapters are found via groups at runtime.

JMB_WSClient (core)

Method / SignalDescription
send_envelope_async(envelope)Send a WS envelope and await the response.
subscribe_to_category_async(category)Subscribe to a message category (e.g. "lobby").
unsubscribe_from_category_async(category)Unsubscribe from a message category.
on_ws_openedSignal — emitted when the WebSocket connects.
on_ws_closed(reason)Signal — emitted when the WebSocket disconnects.
on_ws_message_received(envelope)Signal — emitted for every incoming WS message.
on_failed_to_reconnectSignal — emitted when reconnection attempts are exhausted.

JMB_LobbyAdapter (room browsing)

Method / SignalDescription
list_rooms_async()Fetch all lobby rooms from the server.
create_room_async(vars)Create a new room with the given settings.
join_room_async(room_id, password)Join an existing room (password can be empty).
on_rooms_updated(rooms)Signal — room list refreshed (polled automatically).

JMB_LobbyRoomAdapter (inside a room)

Method / SignalDescription
leave_room_async()Leave the current room.
ready_toggle_async(is_ready)Toggle the local player's ready state.
start_match_async()Request match start (host only).
kick_async(account_id)Kick a player from the room (host only).
setting_change_async(key, value)Update a single room setting.
settings_change_async(settings)Update multiple room settings at once.
migrate_host_async(account_id)Transfer host to another player.
on_room_joined(result)Signal — local player entered a room.
on_room_left()Signal — local player left the room.
on_room_state_changed(room, is_host)Signal — room state updated (members, settings, host).
on_kicked()Signal — local player was kicked.
on_match_start_initiated(countdown)Signal — match countdown started.
on_match_canceled()Signal — match start was canceled.
on_ready_to_enter_gameinst(port)Signal — game instance is ready, connect on this port.

JMB_ChatAdapter (messaging)

Method / SignalDescription
send_message(context)Send a chat message or DM (pass a JMB_CommContext).
on_message_received(context)Signal — incoming chat message or DM.
on_room_members_updated(members)Signal — chat room member list changed.
on_onlineSignal — chat system connected.
on_offlineSignal — chat system disconnected.

JMB_UserManager (auth state)

Method / SignalDescription
login(identifier, password)Log in with email or username.
register(username, password, email)Register a new account and auto-login.
logout()Log out and clear local credentials.
get_account_id()Return the logged-in account ID.
get_display_name()Return the logged-in player's display name.
get_status()Return the current UserState (DISCONNECTED, CONNECTING, CONNECTED).
on_login_completed(result)Signal — login finished (check result.status).
on_register_completed(result)Signal — registration finished.
on_logout_completedSignal — logout finished, credentials cleared.

Network manager overrides

Your script extends JMB_NetworkManager (see part 2), and these are the callbacks you can override. All of them are optional no-ops by default — override only what you need. Server means it fires on the game-server build, Client on player builds, Both everywhere.

JMB_NetworkManager

OverrideDescription
_i_on_self_server_started()Server — the ENet server is listening and the Go handshake completed; the instance is registered and ready for clients. Initialize server-side game state, spawn world objects, start server-only systems.
_i_on_self_server_stopped()Server — the game instance is shutting down. Persist match results, clean up server-side resources.
_i_on_client_verified(client)Server — a client passed identity verification and joined the roster. Spawn the player's character, send initial state, apply permissions.
_i_on_self_connected_to_server()Client — the ENet connection succeeded; verification is still in flight. Show a loading screen, disable input.
_i_on_self_connection_failed()Client — the initial connection attempt failed (bad address, port unreachable, server full). Show an error dialog, offer retry, return to the menu.
_i_on_self_server_disconnected()Client — an established connection was lost. Show a disconnect message, save local state; if an offline scene is set, it loads after this returns.
_i_on_player_joined(peer_id)Both — any peer joined the ENet session. On the server the peer is still unverified here — use _i_on_client_verified() for post-verification logic.
_i_on_player_left(peer_id)Both — any peer left the session. On the server the roster is already updated and reported to the Go backend.
_i_on_clients_synced()Both — the verified client roster synchronized. Refresh player lists, update scoreboards, check if enough players are present.
_i_on_before_scene_changed(old_scene, new_scene, old_data, new_data)Both — a networked scene change is about to begin; the old scene is still loaded. Save transient state, despawn objects, show a loading overlay.
_i_on_scene_changed(new_scene, scene_data)Both — the networked scene change completed and all clients confirmed loading (or timed out). Spawn gameplay objects, start timers, enable input.
_i_on_game_started(config)Both — the session began via the match or world controller; the config carries host id, initial members, and room genvars. Awaitable — the on_game_started signal fires after it returns. Initialize game-mode logic, team assignments.