Your backend bundle and folder layout
Understand what you actually ship to Asobi, scaffold it, and boot it locally so the server answers on your machine.
You will not write a server. Asobi is the server. What you push is a Lua bundle: a small directory of game scripts that Asobi loads and runs. The bundle holds your rules; the platform supplies the database, authentication, matchmaking, and WebSockets around it.
This is the server-authoritative half of the arena backend from Orientation: the client will send intent, but your fighter only moves because a script in this bundle decides it moves.
What is in the bundle
A bundle is one folder. The simplest one has a single mode and looks like this:
my_arena/
├── lua/
│ └── match.lua
└── docker-compose.ymllua/ is the only part Asobi cares about. It is mounted into the runtime and becomes the search path for every callback and every require(). The container is stateless apart from this folder.
| File | Role |
|---|---|
lua/match.lua | One game mode. Config globals at the top, then the callbacks Asobi calls (init, join, leave, handle_input, tick, get_state). With no config.lua, this single file loads as the "default" mode. |
lua/config.lua | Optional multi-mode manifest. Returns a table mapping mode names to match-script paths. When it exists, Asobi reads it instead of a top-level match.lua. Deployment-wide globals such as guest_auth live here, not in the per-mode scripts. |
lua/world.lua | A mode script for a persistent, zoned world instead of an ephemeral match. Same file role as match.lua, but it sets the global game_type = "world" and uses world callbacks. |
lua/bots/*.lua | Optional bot scripts, referenced from a mode's bots = { script = "bots/..." } global. |
other .lua files | Shared modules you require(), resolved relative to lua/. |
There is no separate "assets" or static-file slot in the bundle: everything under lua/ is code that Asobi executes or require()s. Static art and audio live with your client, not the backend.
Config is data at the top of a mode script, not a config file:
-- lua/match.lua
match_size = 2
max_players = 8For a single-mode game those globals sit in match.lua. For a multi-mode game they sit in each mode's script, while deployment-wide settings move to config.lua. The full list of globals and every callback signature are in the Lua scripting guide and Configuration; this track links into them rather than repeating them.
Multi-mode is just more of the same shape:
my_arena/
├── lua/
│ ├── config.lua
│ ├── arena/
│ │ └── match.lua
│ └── world/
│ └── match.lua
└── docker-compose.yml-- lua/config.lua
return {
arena = "arena/match.lua",
world = "world/match.lua"
}We start single-mode. You add config.lua later, in Set up a match and modes, without moving any game logic.
Scaffold it
asobi init my_arena
cd my_arenaThis scaffolds a minimal bundle - lua/match.lua plus a README.md - that you can boot straight away.
Run it locally
asobi dev
asobi dev starts the server against a local Postgres and mounts your lua/ folder live: edit a script, save, and the running server picks it up between ticks. It listens on port 8084 for both HTTP and WebSocket.
Under the bonnet that is nothing more than the asobi_lua image next to a Postgres, so if you would rather run it by hand, the same thing in docker-compose.yml is:
services:
postgres:
image: postgres:17
environment: { POSTGRES_USER: postgres, POSTGRES_PASSWORD: postgres, POSTGRES_DB: my_arena }
healthcheck: { test: ["CMD-SHELL", "pg_isready -U postgres"], interval: 5s }
asobi:
image: ghcr.io/widgrensit/asobi_lua:latest
depends_on: { postgres: { condition: service_healthy } }
ports: ["8084:8084"]
volumes: ["./lua:/app/game:ro"]
environment: { ASOBI_DB_HOST: postgres, ASOBI_DB_NAME: my_arena }docker compose up -d
Either way you now have the same server your players will hit, running on your machine. Migrations run on first boot; there is nothing to set up in Postgres by hand.
Deploy later (aside)
You do not deploy anything yet. When you do, the bundle is identical on both paths and so is every client call except the base URL. Only where it runs, and where its secrets and database come from, differs:
- Cloud (or push from console.asobi.dev): you upload the same
lua/folder. The command names the target environment and the bundle directory:asobi deploy prod luaThe per-project Postgres and the guest-auth pepper are provisioned per environment automatically; you never touch a database or a secret. You write no config file at all.
- Self-hosted (your own release of
asobi+asobi_lua, your own Postgres): you run theasobi_luaimage (or embed asobi as an Erlang dependency), point it at your Postgres, and set theASOBI_*environment variables yourself. Guest auth additionally needs you to supply the pepper viaASOBI_GUEST_VERIFIER_PEPPER. See Self-hosting and Configuration.
Checkpoint
Boot the server, then prove it answers. With asobi dev (or docker compose up -d) running:
curl -s localhost:8084/api/v1/auth/register \
-H 'content-type: application/json' \
-d '{"username":"alice","password":"hunter2!"}'You should get JSON back with a player_id:
{ "username": "alice", "player_id": "019de3...", ... }A player_id in the response means the bundle loaded, the database is wired up, and the server is reachable on your machine. That is the whole step.
Next: Step 2 - Install the client SDK
Learn how and when it talks to the server.