Asobi - Open Source Multiplayer Game Backend for Defold

Hey everyone!

I’ve been building Asobi, an open source multiplayer game backend written in Erlang/OTP, and I wanted to share it with the Defold community since we have a Defold SDK and a playable demo.

What is Asobi?

Asobi is a server-authoritative game backend that handles the boring parts of multiplayer so you can focus on your game:

  • Authentication - registration, login, session tokens
  • Matchmaking - queue-based with pluggable strategies (fill, skill-based, custom)
  • Real-time WebSocket - 10Hz game state broadcast, input at 60fps
  • Leaderboards, chat, presence, economy, inventory
  • Voting system - let players vote on game events between rounds
  • Bot AI - automatic queue filling with Lua-scripted bots
  • Lua scripting - write your game logic in Lua, no Erlang needed

The backend runs on the BEAM VM (Erlang), which gives you massive concurrency, fault tolerance out of the box.

Lua Scripting - No Erlang Required

This is the part I’m most excited about for Defold developers. You can write your entire server-side game logic in Lua:

function init(config)
    return { players = {}, projectiles = {} }
end

function join(player_id, state)
    state.players[player_id] = {
        x = math.random(800), y = math.random(600),
        hp = 100, speed = 4
    }
    return state
end

function handle_input(player_id, input, state)
    local p = state.players[player_id]
    if input.right then p.x = p.x + p.speed end
    if input.left then p.x = p.x - p.speed end
    if input.shoot then
        -- create projectile...
    end
    return state
end

function tick(state)
    -- physics, collisions, win conditions
    return state
end

You implement a few callbacks (init, join, leave, handle_input, tick, get_state) and Asobi handles everything else - matchmaking, WebSocket transport, bot spawning, authentication.

Arena Demo

To prove it works, we built a top-down arena shooter:

The game features:

  • 4-10 players per match (bots fill empty slots)
  • WASD movement, mouse aim, click to shoot
  • 90-second rounds with projectile combat
  • Between-round upgrades (boons) for top players
  • Player voting on round modifiers (Double Damage, Tight Quarters, etc.)
  • Ship sprites and cannonball projectiles

Running it locally

# Clone the game server (just Lua scripts + docker-compose)
git clone https://github.com/widgrensit/asobi_arena_lua
cd asobi_arena_lua
docker compose up -d

# Clone and build the Defold client
git clone https://github.com/widgrensit/asobi-defold-demo
cd asobi-defold-demo
ln -s /path/to/asobi-defold/asobi asobi
java -jar bob.jar --platform x86_64-linux resolve build
./build/x86_64-linux/dmengine

The client has a server select screen - choose LOCAL to connect to your Docker instance.

Defold SDK

The asobi-defold SDK is a pure Lua module (no native extensions) that gives you:

  • HTTP client for REST APIs (auth, leaderboards, inventory, etc.)
  • WebSocket client for real-time gameplay
  • Matchmaker integration
  • Chat and presence

Drop it into your project as a symlink or copy and you’re good to go.

A note on AI

Full transparency - I used AI (Claude) to help write the Lua game scripts. The backend platform (Asobi) is more hand-written Erlang but also some AI, but for the arena demo’s Lua code, AI was a great pair programmer.

Links

Everything is open source. Would love to hear your feedback and see what you build with it!

14 Likes

Wow, very cool! Thank you for sharing!

What are your plans for this? Are you creating a game of your own on Asobi?

3 Likes

Mostly I wanted to show that Erlang/otp is good for backend.

Maybe depending how things go I will look into hosting service.

But mainly it is fun to build this and see how I maybe can help the gaming community and do commercial for Erlang. :slight_smile:

2 Likes

So if anyone want to try this and need help I would be happy to try to build a game with the backend.

Is websocket scalable or I need to do redis part myself ? I saw you use OTP pg module as pub/sub. I didnt use pg ever

WebSocket (with session cache)

Metric Value
Connections 4,613 (0 failures)
Messages sent+received 922,600
Throughput 49K msg/sec
RTT p50 10.7ms
RTT p99 31.3ms
Memory per connection ~15KB
Delivery 100%

Without session cache: 3,522 connections (1,024 failures) — the cache eliminated the auth DB bottleneck. This was that each connection needed to check auth to be sure that the websocket was still a authenticated user. Now the authentication is cached in the node after the first db check.

Key caveats: single node, 8 cores, client and server sharing the same machine (so client steals ~50% CPU). Real deployment estimated at 2-3x higher throughput.

I haven’t tried multi-node yet, it might be slower. But asobi uses postgres job queue for fan out. So a message that needs to be broadcasted is sent to each node that then handles it internally with erlang messages and websockets.

Idea is to try to have a simple setup host + postgres and that should help you started.

1 Like

15k concurrent connections on a single node looks really solid as a starting point. I think even 1gb ram is enough for this setup

1 Like

I hosted asobi arena play.asobi.dev on fly.io it is two machines with 1cpu shared 1gb. Then they have postgres they share, 4gb ram or so.

I haven tried loadtesting it there but I think benchmarking is hard to get it right.

3 Likes

If anyone want to try Asobi in a game jam or so I am looking into hosting it.

The idea is still very early. :slightly_smiling_face:

2 Likes

Updates

Quite a lot has happened since the initial post, so here’s a summary of what’s new.

asobi_lua — Standalone Lua Runtime

The Lua scripting layer has been extracted into its own standalone OTP application (asobi_lua). This is the bit you actually deploy — a single Docker image (ghcr.io/widgrensit/asobi_lua:latest) that runs your game logic without touching any Erlang.

It now ships with a full game.* Lua API that gives your scripts access to all engine systems:


-- Economy

game.economy.grant(player_id, "gold", 100, "quest_reward")

game.economy.debit(player_id, "gold", 50, "shop_purchase")

local wallets = game.economy.balance(player_id)

-- Leaderboards

game.leaderboard.submit("weekly_kills", player_id, score)

local top10 = game.leaderboard.top("weekly_kills", 10)

local nearby = game.leaderboard.around("weekly_kills", player_id, 5)

-- Storage (cloud saves)

game.storage.player_set(player_id, "progress", "checkpoint_3", data)

local save = game.storage.player_get(player_id, "progress", "checkpoint_3")

-- Messaging

game.broadcast("boss_spawned", { zone = "north", hp = 5000 })

game.send(player_id, { type = "quest_update", step = 3 })

game.notify(player_id, "reward", "You earned 100 gold!")

game.chat.send(channel_id, sender_id, "gg")

World Server — Large-Scale Spatial Multiplayer

The biggest addition is asobi_world — a new game mode for persistent, zone-based worlds (think survival games, MMO-lite, open worlds). The match system is still there for session-based games, but worlds support:

  • Zone partitioning — the world is split into grid zones, each ticking in parallel

  • Interest-based broadcasting — players only receive updates from nearby zones

  • Entity persistence — zone state snapshots to PostgreSQL, survives crashes and restarts

  • Dynamic spawning — template-based entity spawning with respawn rules, jitter, and max limits

  • Spatial queriesquery_radius, query_rect, nearest, in_range for combat, loot, proximity

  • NPC zone crossing — entities transfer between zones at boundaries

  • Persistent worlds — world stays alive even when all players leave

The Defold SDK has been updated with world.list, world.create, world.join, world.find_or_create and event handlers for phase_changed and world_finished.

Phases & Timers

Games can now define multi-phase progression with an optional phases callback:


function phases(config)

return {

{ name = "warmup", duration = 30 },

{ name = "active", duration = 180, start_condition = { type = "player_count", min = 4 } },

{ name = "overtime", duration = 60 }

}

end

function on_phase_started(phase_name, state)

if phase_name == "active" then

game.broadcast("phase", { name = "active", message = "Go!" })

end

return state

end

There are also entity timers for per-entity cooldowns, crafting, decay — ticked as part of the zone loop so they scale to thousands without overhead.

Reconnection & Recovery

  • Configurable grace period — player entity stays alive while disconnected

  • Zone state backed up to ETS every ~1 second, recovered on process crash

  • Entity persistence to PostgreSQL for node-level recovery

Security & Production Hardening

  • Per-connection rate limiting (60 msg/sec token bucket)

  • WebSocket payload size limit (64KB)

  • Input validation preventing handler crashes

  • HTTP security headers (HSTS, X-Frame-Options, CSP, etc.)

  • Chat content length limits

If anyone wants to try building something with it, the Defold SDK is a single Lua module — happy to help get you set up.

6 Likes

I have now improved the documentation with Lua also.

2 Likes

Does websocket have rate limit feature? How do you protect from spam?

Right now it is handled in Asobi.

Per-connection msessage rate limit:
60 messages / 1000 ms
65,536 bytes max per message

Chats have a 2000 bytes cap.

1 Like