Build an MMO on Robutler
You can build a massively-multiplayer game with no game server of your own and ship it as an app anyone can fork and re-theme. The platform provides the hard parts as ordinary SDK surface; your game is a world definition, a pure simulation step, and a renderer.
| Need | Platform primitive |
|---|---|
| Identity you can trust | host.user, room tokens bound to a user id |
| An arbiter for contested state | Authoritative rooms - your pure step runs server-side, clients send intents |
| Collaborative (non-contested) surfaces | host.collab - Yjs CRDT docs, written by CLIENTS, for symmetric DOCUMENTS beyond any match (clan pages, shared notes, guild boards). Chat and lobby directories do NOT need Yjs - see below. The sim never writes Yjs; a pure game usually needs none |
| Low-latency cosmetic positions | host.collab.getTurnCredentials + a multi-party RTC overlay (optional) |
| Custody of anything valuable | host.fn server functions - currency, minting, transfers |
The one idea that makes it scale: shard the world
Do not put the whole world in one room. Partition it into fixed-size
square regions, one room each, addressed by a code derived from the
region's integer coordinates (e.g. R + zigzag-base36 of (rx, ry) - any
[A-Z0-9]{4,12} code works as a room code). A player joins only the region
they stand in, plus neighbor regions when inside an edge buffer (1 room in
the interior, 2 at an edge, 4 at a corner). Make the edge buffer larger than
your longest attack range, so anything that can hit you comes from a room you
have already joined.
Why this is the whole game: cost concentrates within a region, but regions are independent, so total load across the world is linear in players and scales only with local density - which you cap by design (~30 co-present players reads as a crowd). A million players spread across forty thousand regions cost the same per region as five thousand.
Each region room is an authoritative room: declare a
region room in rooms.json, and your module's step runs the region's
simulation - mobs, structures, drops, resource depletion - server-side.
Region rooms checkpoint and rehydrate automatically: an empty region's
room disposes, its state persists, and the next visitor resumes it. Dormant
wilderness costs nothing. Declare persist: true plus a stateVersion in
the room's manifest to make that persistence PERMANENT: every dispose
writes the final checkpoint through to the platform's durable store, a
create that misses the hot cache reads it back, and the fence is your
version (not the bundle hash) - player-built structures survive code
updates, cache loss, and unbounded dormancy. checkpointTtlSec sizes only
the hot cache in front.
Entities cross borders by handover, not double-simulation. When your
step notices a mob past the region box, emit a room.transfer effect to the
neighbor's room code: the payload arrives in that room's next tick as a
$transfer intent, and if the neighbor is dormant the platform answers with
a $result echo so you can bounce the walker back instead of losing it.
Exactly ONE room simulates any entity at any moment - a chase follows the
player across the shard line without any region ever running twice.
Free geography: derive, don't store
Make terrain, resources, and landmarks a pure function of
(worldSeed, regionCoords). Geography is then never stored or sent: every
client derives the identical world, the server derives the same one inside
the room module (import the same generator file), and a fork inherits it all
by changing one seed.
Durable, contested, and ephemeral state
Three kinds of state, three homes. Getting this split right is most of the architecture:
- Contested (mobs, hit points, structures' existence, drops, anything a
cheater would want to write): the region room's state. Clients send
intents (
pos,hit,build,pick); the sim decides. Late joiners get it automatically. - Collaborative beyond a match (clan pages, shared notes):
host.collabYjs docs - written by clients, merged by the CRDT, right wherever participants are symmetric, nothing needs an arbiter, and the surface is a DOCUMENT. Two classic "Yjs" surfaces are better served by rooms: a global chat is a cheap low-tick room whose state is a message ring (late joiners get history from the ordinary snapshot), and a lobby directory is the platform's$lobbylistings (see host-rooms). The room's step NEVER writes Yjs (its outputs are state and effects), so most pure games need no Yjs at all. - Ephemeral presence (positions, headings, animation): in a region room
this is simply part of room state - send a position intent at ~10Hz and
render others from the kit's
interpolated()view. If you need lower-latency cosmetic motion between snapshots, add the optional WebRTC overlay for position frames - keep it strictly cosmetic; the room stays the authority for every outcome.
Follow the four smoothness rules
- latch inputs server-side, render remotes interpolated, predict your own entity with a dead zone, play action feedback locally.
Border crossings
Join the destination region before you cross (the edge buffer gives you the window), keep publishing your position into both rooms while inside the buffer, and release the old room only after a grace period (10-15s) so border pacing never churns joins. Three rules make the buffer seamless: publish positions AND action claims (hits, pvp) to EVERY joined room - each sim ignores ids it does not own, so the fan-out is idempotent and a shot at a mob just across the line lands in the sim that owns it; key player identity on the DURABLE user id in whatever you render from (seats are per-room - deduping by seat would draw border ghosts twice); and route world writes (builds) to the room whose bounds CONTAIN the spot, which near a border is not always the room you stand in. Ref-count your desired-region set from position every frame; the join/leave mechanics belong in one small manager module with an injected join function, so swapping transports never touches game code.
Trust model
Rule zero: anything of value is a server function, not game state.
Balances, minting, and transfers go through host.fn,
where no client - and no room module - can reach beyond its authorization.
With region rooms, contested world state gets the same treatment
automatically: a client cannot write a kill, a structure, or a pickup - it
can only ask, and the server-side sim decides. MOVEMENT can be
authoritative too, cheaply: geography derives from the seed (~1ms per
region to generate, sub-microsecond per walkability query), so the room
integrates a direction latch (mv {x, y, sp, seq}) through the same move
core the client predicts with - position claims die entirely. Round it out
with a magnitude-capped imp impulse for knockback feel and a
rate-limited warp whose target must be real ground (respawns and match
teleports stay possible, teleport hacks do not); demote pos to
cosmetics (heading, animation). Whatever you still leave soft, the blast
radius of lying is bounded by the room's range checks running against its
own authoritative state.
AI-driven NPCs
Drive NPCs with a scripted finite-state-machine floor that always works, and
optionally an LLM layer for flavor and decisions. Keep deterministic NPC
physics inside the room's step (pure, replayable); treat an inference
result as an input event the sim consumes, never as computation inside
the fold. On-device inference (host.infer) can run
client-side and enter as an intent; server-side NPC brains arrive with the
rooms fn.call effect.
Ship it: the remix path
- Scaffold a widget; declare
collab: trueand arooms.jsonwith yourregionroom. - Write the world as pure modules: a terrain generator keyed by seed, a sim
step, a renderer. - Keep value in
host.fnserver functions. - Publish like any app - a fork needs no server provisioning: geography is a seed, contested state is a room the platform hosts, durable documents are CRDTs the platform hosts.
See also
- Authoritative rooms - the room contract, intents, effects, visibility, smoothness
host.collab- world documents and lobby codes- Multi-party RTC - the optional cosmetic overlay
host.infer- on-device LLM for NPCshost.fn- server functions for anything valuable