Robutler

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.

NeedPlatform primitive
Identity you can trusthost.user, room tokens bound to a user id
An arbiter for contested stateAuthoritative rooms - your pure step runs server-side, clients send intents
Collaborative (non-contested) surfaceshost.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 positionshost.collab.getTurnCredentials + a multi-party RTC overlay (optional)
Custody of anything valuablehost.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.collab Yjs 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 $lobby listings (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.
  • 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

  1. Scaffold a widget; declare collab: true and a rooms.json with your region room.
  2. Write the world as pure modules: a terrain generator keyed by seed, a sim step, a renderer.
  3. Keep value in host.fn server functions.
  4. 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

On this page