
§ 01 Open-source game backend on the BEAM
A multiplayer game backend built on Erlang/OTP. Fault-tolerant by design. Zero-downtime deploys. 100K+ concurrent connections per node.
Asobi is early but fully open-source and ready to play with. Spin it up, prototype your next game, and help shape the future of game backends on the BEAM.
02 / Demo
Edit your match logic in Lua. The running match picks up the change on the next tick. No restart, no reconnect, no kicked players.
Run it locally:
git clone https://github.com/widgrensit/asobi
cd asobi/examples/hotreload-demo
docker compose upAbout 90 seconds. Open http://localhost:3000, drive a cube with WASD, then edit lua/match.lua and save.
03 / Client
Connect and receive match state in 15 lines — pick your engine.
UAsobiClient* Client = NewObject();
Client->SetBaseUrl(TEXT("http://localhost:8080"));
UAsobiAuth* Auth = NewObject();
Auth->Init(Client);
FOnAsobiAuthResponse OnLogin;
OnLogin.BindDynamic(this, &AMyPawn::OnLoggedIn);
Auth->Login(TEXT("player1"), TEXT("secret"), OnLogin);
// After login:
WebSocket->Connect(TEXT("ws://localhost:8080/ws"));
WebSocket->Authenticate(Client->GetAuthToken());
Matchmaker->Add(TEXT("arena"), {}, OnQueued);
WebSocket->OnMatchState.AddDynamic(this, &AMyPawn::OnMatchState); var asobi = new AsobiClient("localhost", port: 8080);
await asobi.Auth.LoginAsync("player1", "secret");
asobi.Realtime.OnMatchState += state =>
Debug.Log($"tick {state.tick}");
await asobi.Realtime.ConnectAsync();
await asobi.Matchmaker.AddAsync("arena");
// When matched, server sends match.joined → match.state at tick rate.@onready var asobi: AsobiClient = $AsobiClient
func _ready():
await asobi.auth.login("player1", "secret")
asobi.realtime.match_state.connect(_on_state)
asobi.realtime.connect_to_server()
asobi.realtime.add_to_matchmaker("arena")
func _on_state(payload: Dictionary):
print("tick ", payload.get("tick"))local asobi = require("asobi.client")
local rt = require("asobi.realtime")
asobi.auth.login("player1", "secret", function(res)
rt.init(asobi)
rt.on("match_state", function(payload)
print("tick " .. payload.tick)
end)
rt.connect(function()
rt.matchmaker_add("arena")
end)
end)import { Asobi } from "@asobi/client";
const asobi = new Asobi({ baseUrl: "http://localhost:8080" });
const { access_token } = await asobi.auth.login({
username: "player1", password: "secret",
});
asobi.client.setToken(access_token);
const ws = asobi.websocket();
ws.on("match.state", (s) => console.log("tick", s.tick));
await ws.connect();
await asobi.matchmaker.add({ mode: "arena" });final asobi = AsobiClient(host: 'localhost', port: 8080);
await asobi.auth.login('player1', 'secret');
asobi.realtime.onMatchState.listen((state) {
print('tick ${state.tick}');
});
await asobi.realtime.connect();
await asobi.matchmaker.add(mode: 'arena');-- Server-side game mode (runs inside asobi_lua)
function on_player_joined(match, player)
game.broadcast(match, "welcome", { player_id = player.id })
end
function on_match_input(match, player, input)
player.x = player.x + (input.move_x or 0)
player.y = player.y + (input.move_y or 0)
-- match.state is diffed and broadcast automatically each tick.
end04 / Models
Your game decides, not the backend. Both models share the same auth, social, economy, and storage.
Small, ephemeral game sessions assembled by the matchmaker. Players queue, the server forms a match, a game mode runs, results are recorded. Like FTL, Brotato-with-friends, arena shooters, card games.
Long-running worlds with lazy-loaded zones and streamed terrain. Find-or-create joins a world that isn’t full; players drop in and out. Like EVE, Albion, MMOs, shared sandboxes.
05 / Runtime
The same virtual machine that powers WhatsApp, Discord, and RabbitMQ. Designed for millions of concurrent connections with predictable latency.
■ 58 processes up · ■ 2 supervised restart · zero impact to neighbours
Each match runs in its own process with isolated garbage collection. No stop-the-world pauses affecting other players.
The BEAM scheduler ensures fair CPU time for every match. One expensive operation cannot starve others.
If a match process crashes, it restarts automatically. Players reconnect to a fresh state. The server is unaffected.
Graceful shutdown, health endpoints, and rolling deploys out of the box. Built for Kubernetes, Fly.io, and any container orchestrator.
Lightweight processes and efficient I/O multiplexing. Handle half a million concurrent WebSocket connections per node.
OpenTelemetry integration, structured logging, and Telemetry events. Monitor every match, queue, and connection.
06 / Kit
A complete backend for multiplayer games. One release, no external dependencies.
Player registration, login, sessions, and OAuth. Built on nova_auth.
Automatic player pairing with configurable rules. Lobby and queue support.
WebSocket-based state synchronization at configurable tick rates.
Ranked scoring with ETS-backed storage. Per-game, per-season, global.
Wallets, transactions, inventory, and in-game store.
Friends, groups, chat, and notifications.
Persistent player data storage with versioning.
Web-based management UI for players, matches, and economy.
07 / Clients
Official client libraries with full API coverage. Pick your engine and start building.
UE 5.7+ plugin. Blueprint-callable subsystems.
View guideUnity 2021.3+. Install via UPM git URL.
View guideGodot 4.x addon. Enable in Project Settings.
View guideAdd as dependency in game.project.
View guideBrowser + Node.js 18+. Event-emitter WS API.
View guideWorks with Flutter, Flame, and standalone Dart.
View guideHot-reloaded game modes hosted by asobi_lua.
View guideAll SDKs cover auth, matchmaking, real-time, leaderboards, economy, social, storage, and more.
08 / Contract
Implement the asobi_match behaviour. Asobi handles the rest.
-module(arena_match).
-behaviour(asobi_match).
-export([init/1, handle_join/3, handle_input/3,
handle_tick/2, handle_leave/3]).
init(Opts) ->
#{max_players => maps:get(max_players, Opts, 8),
tick_rate => 10,
players => #{},
projectiles => []}.
handle_join(PlayerId, _Metadata, State) ->
Spawn = random_spawn_point(),
Player = #{pos => Spawn, hp => 100, score => 0},
{ok, State#{players := maps:put(PlayerId, Player,
maps:get(players, State))}}.
handle_input(PlayerId, #{<<"action">> := <<"fire">>} = Input, State) ->
Projectile = spawn_projectile(PlayerId, Input),
{ok, State#{projectiles := [Projectile |
maps:get(projectiles, State)]}}.
handle_tick(_DeltaMs, State) ->
S1 = move_projectiles(State),
S2 = detect_collisions(S1),
{broadcast, S2}.09 / Position
Honest accounting against the engines you’d otherwise pick.
| Asobi | Nakama | Colyseus | |
|---|---|---|---|
| Runtime | BEAM (Erlang/OTP) | Go | Node.js |
| Garbage Collection | Per-process, isolated | Stop-the-world | Stop-the-world |
| Fault Tolerance | OTP supervision trees | Manual recovery | Manual recovery |
| Cloud/K8s | Graceful shutdown, health checks | Basic support | Manual setup |
| Pub/Sub | Built-in (pg module) | Requires Redis | Built-in |
| Connections/Node | 100K+ | ~50K | ~10K |
| Observability | OpenTelemetry + Telemetry | Prometheus | Custom metrics |
| License | Apache 2.0 | Apache 2.0 | MIT |
10 / Start
Four commands between you and a running multiplayer game.
rebar3 nova new my_game fullstack
{asobi, "~> 0.1"}
Define a module with the asobi_match behaviour
rebar3 nova serve
11 / People
Ask questions, share what you're building, and help shape Asobi.
Join us on Discord