The goal
I wanted a home for the small games I build, where playing a friend is one click and not a download. That meant real accounts, a lobby that updates live, and online matches that feel fair. It also had to cost close to nothing to run while it is small.
The shape of it
One Worker serves the static site and a small API. Anything that needs to remember state lives in a Durable Object: a single instance of a class that Cloudflare keeps running, with its own storage.
- Hub is one instance with its own SQLite database. It holds accounts, sessions, the live lobby, match records and results.
- Room is created per 5-character code. It relays messages between exactly two sockets, host and guest. A third socket is told the room is full.
- Arena runs Knife Duels matches, from 1v1 up to 4v4.
From challenge to match
- You open the Players tab. Your browser keeps one WebSocket open to the Hub, which tells everyone who is online.
- You challenge someone. They see it straight away and accept or decline.
- On accept, the Hub makes a room code and sends both players the go.
- Both games join that Room, which relays inputs between them.
- When the match ends, both players' games report the result. It only counts if they agree.
The lobby socket uses the WebSocket hibernation API, so a quiet lobby does not keep the Hub awake and billing while people just sit on the page.
Keeping it fair
Running every match on the server is too expensive for a small site, so I split the trust instead. In Oathsteel, the sword duel, the host decides hits and rounds, each player owns their own movement, and the other fighter is driven by relayed inputs. Movement stays responsive on both ends, and one side has the final say on anything that decides a winner.
Results only reach the leaderboard for matches made in the lobby, and only when both players' games report the same outcome. Knife Duels has more players per match, so a result counts once, when most of the human players agree on it.
Accounts done properly
Passwords are hashed with PBKDF2-SHA256 at 100,000 iterations. Session tokens are stored hashed too, so a leaked database would not hand out anyone's login.
Testing multiplayer
The Room and the Hub have automated checks that run against a local Worker: 8 for the relay, and 21 for accounts, the lobby and results. Knife Duels keeps its game logic apart from three.js and the page, so it can be unit tested on its own, with 178 tests.
For the real thing, a script drives two headless browsers through a full online duel. The catch is that two software-rendered browsers on one machine starve each other, so they run far below real speed and every wait in the test has to be generous.
What I took from it
- Durable Objects make multiplayer state easy to reason about: one object, one place the truth lives.
- Adding a game is a folder and one line in a list, which is why the hub keeps growing.
- No build step and a self-hosted three.js mean the site deploys in seconds and has nothing to break between releases.