Configuration reference
All Asobi config lives under the asobi OTP application. Set it via sys.config (releases), config.exs/sys.config.src (env-var templating), or application:set_env/3 at runtime.
Database
{kura, [
{repo, asobi_repo},
{backend, kura_backend_postgres},
{host, "localhost"},
{port, 5432},
{database, "asobi"},
{user, "postgres"},
{password, "postgres"},
{pool_size, 10}
]}Asobi runs on Kura 2.x with pluggable backends. Add kura_postgres to your rebar.config deps and the backend key tells Kura which one to use. Swap to kura_backend_sqlite for an embedded setup.
Environment variables (consumed via sys.config.src): ASOBI_DB_HOST, ASOBI_DB_NAME, ASOBI_DB_USER, ASOBI_DB_PASSWORD.
HTTP / WebSocket
{nova, [
{cowboy_configuration, #{port => 8080}},
{plugins, [
{pre_request, nova_cors_plugin, #{allow_origins => <<"*">>}}
%% ... other pre/post request plugins
]}
]}Game modes (matches and worlds)
All per-mode config — whether a mode is a match or a world, which module implements it, match size, tick rate, bots, spatial config — lives under the single game_modes map.
{asobi, [
{game_modes, #{
~"arena" => #{
module => arena_game,
match_size => 4,
max_players => 4,
tick_rate => 50, %% ms per tick (default 100)
strategy => fill, %% fill | skill_based | module()
bots => #{enabled => true, min_players => 4,
script => ~"bots/arena.lua"}
},
~"world1" => #{
type => world,
module => {lua, ~"world1/match.lua"},
grid_size => 10,
zone_size => 200,
view_radius => 1,
tick_rate => 50,
persistent => false,
lazy_zones => true,
zone_idle_timeout => 30000,
max_active_zones => 10000
}
}}
]}Matchmaker
{asobi, [
{matchmaker, #{
tick_interval => 1000,
max_wait_seconds => 60
}}
%% Strategy is per-mode — see the `strategy` key under game_modes.
]}Voting
{asobi, [
{vote_templates, #{
<<"default">> => #{method => <<"plurality">>, window_ms => 15000,
visibility => <<"live">>},
<<"boon_pick">> => #{method => <<"plurality">>, window_ms => 15000}
}}
]}Authentication
{asobi, [
{base_url, ~"https://api.example.com"}, %% used for OIDC redirects
{oidc_providers, #{
google => #{issuer => ~"https://accounts.google.com",
client_id => ~"...", client_secret => ~"..."},
apple => #{issuer => ~"https://appleid.apple.com",
client_id => ~"...", client_secret => ~"..."}
}},
{steam_api_key, ~"..."},
{steam_app_id, ~"..."},
{apple_bundle_id, ~"com.example.game"},
{google_package_name, ~"com.example.game"},
%% Apple StoreKit 2 receipt verification — root CA used for x5c chain validation.
%% Defaults to priv/apple_root_ca.pem inside the asobi app.
{apple_root_ca_path, ~"/etc/asobi/apple_root_ca.pem"}
]}Rate limits
Per-route limits enforced by asobi_rate_limit_plugin. Defaults: 5 req/sec/IP on auth, 10 on IAP, 300 elsewhere. The auth limiter is the brute-force gate — a 5/sec cap plus the bcrypt cost on login makes online password guessing infeasible at internet scale.
{asobi, [
{rate_limits, #{
auth => #{limit => 5, window => 1000},
iap => #{limit => 10, window => 1000},
api => #{limit => 300, window => 1000}
}}
]}The dev/test sys config bumps all three to 1000 because CT bursts register/login calls against 127.0.0.1.
World capacity
Caps on persistent worlds (world server). When a player tries to create a world beyond the per-player cap, the API returns 429; when the global cap is hit, 503.
{asobi, [
{world_max_per_player, 5}, %% default 5
{world_max, 1000} %% default 1000
]}Terrain provider allowlist (asobi_lua only)
A Lua script returning { module = "<some_atom>", ... } from terrain_provider/1 must name a module on this allowlist. Defaults to the two built-in providers; widen explicitly if you ship a custom one.
{asobi_lua, [
{terrain_providers, [asobi_terrain_flat, asobi_terrain_perlin]}
]}Per-call upper bounds
These limits exist to bound the cost of a single hostile request and are not currently runtime-tunable. See the security guide for the rationale.
Endpoint / surface | Limit
-----------------------------|------------------------------------------------
Cloud save body | 256 KB
Cloud save slots / player | 10
Inventory consume quantity | 1 .. 1_000_000
Leaderboard top ?limit | 1 .. 100
Leaderboard around ?range | 1 .. 50
Chat history ?limit | 1 .. 200
DM content | 2000 bytes
WS chat channels / conn | 32
Idle channel timeout | 60 s
Lua decode depth | 64 levelsLeaderboards
Leaderboards are spawned per-board on demand — there is no config map. Start one eagerly with asobi_leaderboard_sup:start_board/1, or just call asobi_leaderboard_server:submit/3 and the first hit will spawn it.
Lua runtime
{asobi, [
{game_dir, "/app/game"} %% where asobi_lua_config looks for match.lua / config.lua
]},
{asobi_lua, []}Clustering
{asobi, [
{cluster, #{
strategy => dns, %% dns | epmd
dns_name => ~"asobi-headless", %% DNS A record (for `dns`)
hosts => [], %% list of hosts (for `epmd`)
poll_interval => 10000 %% ms between discovery polls
}}
]}Telemetry & logs
{kernel, [
{logger, [
{handler, default, logger_std_h, #{
level => info,
formatter => {nova_jsonlogger_formatter, #{}}
}}
]}
]}Common env vars
These are the variables consumed by the published asobi_lua image's sys.config.src:
ASOBI_PORT HTTP/WebSocket listen port (required)
ASOBI_DB_HOST Postgres host
ASOBI_DB_NAME Postgres database name
ASOBI_DB_USER Postgres user
ASOBI_DB_PASSWORD Postgres password
ASOBI_CORS_ORIGINS Comma-separated allowed origins for CORSWhere next?
- Self-host
- Clustering
- Performance tuning
- Security — threat model and the rationale behind the caps above.