
Preview - v0.1
§ 01 Open-source game backend on the BEAM
Your game
never goes down.
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. Save. Live.
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
Same server, any engine
Connect and receive match state in 15 lines — pick your engine.
UAsobiClient* Client = NewObject<UAsobiClient>();
Client->SetBaseUrl(TEXT("http://localhost:8084"));
UAsobiAuth* Auth = NewObject<UAsobiAuth>();
Auth->Init(Client);
FOnAsobiAuthResponse OnLogin;
OnLogin.BindDynamic(this, &AMyPawn::OnLoggedIn);
Auth->Login(TEXT("player1"), TEXT("secret"), OnLogin);
// After login:
WebSocket->Connect(TEXT("ws://localhost:8084/ws"));
WebSocket->Authenticate(Client->GetAuthToken());
Matchmaker->Add(TEXT("arena"), {}, OnQueued);
WebSocket->OnMatchState.AddDynamic(this, &AMyPawn::OnMatchState);var asobi = new AsobiClient("localhost", port: 8084);
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 "@widgrensit/asobi";
const asobi = new Asobi({ baseUrl: "http://localhost:8084" });
const { access_token } = await asobi.auth.register({
username: "player1", password: "secret",
});
const ws = asobi.websocket({ token: access_token });
ws.on("match.state", (s) => console.log("tick", s.tick));
await ws.connect();
ws.sendFire("matchmaker.add", { mode: "arena" });final asobi = AsobiClient(host: 'localhost', port: 8084);
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
Sessions or worlds - pick the right shape
Your game decides, not the backend. Both models share the same auth, social, economy, and storage.
Sessions
Matches
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.
- Matchmaker with skill + party rules
- Server-authoritative tick (30-60 Hz)
- Auto-cleanup when the match ends
- In-match voting, DMs, chat
Persistent
Worlds
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.
- Lazy zones scale on demand
- Binary terrain chunks on join
- ETS-backed reconnection state
- 500+ concurrent players per world
05 / Runtime
Built on the BEAM
The same virtual machine that powers WhatsApp, Discord, and RabbitMQ. Built for massive concurrency with predictable latency.
■ 58 processes up · ■ 2 supervised restart · zero impact to neighbours
⚙
Per-Process GC
Each match runs in its own process with isolated garbage collection. No stop-the-world pauses affecting other players.
⚡
Preemptive Scheduling
The BEAM scheduler ensures fair CPU time for every match. One expensive operation cannot starve others.
🛡
OTP Supervision
If a match process crashes, it restarts automatically. Players reconnect to a fresh state. The server is unaffected.
☁
Cloud Native
Graceful shutdown, health endpoints, and rolling deploys out of the box. Built for Kubernetes, Fly.io, and any container orchestrator.
📈
100K+ Connections
Lightweight processes and efficient I/O multiplexing. Handle 100K+ concurrent WebSocket connections per node.
📊
Built-in Observability
OpenTelemetry integration, structured logging, and Telemetry events. Monitor every match, queue, and connection.
06 / Kit
Everything you need
A complete backend for multiplayer games. One release, no external dependencies.
Authentication
Player registration, login, sessions, and OAuth. Built on nova_auth.
Matchmaking
Automatic player pairing with configurable rules. Lobby and queue support.
Real-Time Sync
WebSocket-based state synchronization at configurable tick rates.
Leaderboards
Ranked scoring with ETS-backed storage. Per-game, per-season, global.
Virtual Economy
Wallets, transactions, inventory, and in-game store.
Social
Friends, groups, chat, and notifications.
Cloud Saves
Persistent player data storage with versioning.
Admin Dashboard
Web-based management UI for players, matches, and economy.
07 / Clients
SDKs for every engine
Official client libraries with full API coverage. Pick your engine and start building.
All SDKs cover auth, matchmaking, real-time, leaderboards, economy, social, storage, and more.
08 / Contract
Define your game logic
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
How Asobi compares
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
Get started in minutes
Sign up on the console, then four commands to a live game.
1
Sign up on the console
console.asobi.dev
2
Authenticate the CLI
asobi login
3
Create an environment
asobi create prod
4
Deploy your Lua
asobi deploy prod game/