Multiplayer 3D web game
MoonLite
MoonLite is a free multiplayer 3D "after-hours park": strangers make music together on a shared timeline, watch films under an open sky, race go-karts, bowl and play chess. I built all of it alone, from the netcode and the audio engine to the servers and the legal pages, and took it from first commit to public launch in about three weeks.
Access from Türkiye is currently restricted; the site and game open normally from other countries.
The challenge
Crowded 3D rooms usually die the same way: one player moves, the server tells everyone, everyone moves, the server tells everyone. Per-event fan-out grows with the square of the room size.
Worse, crowd traffic shares a channel with the one thing that must never glitch: music. If an emote storm can delay a mix command, the product is broken. And it all had to run on hardware an indie studio can afford.
Key decisions
- 01
Three traffic planes that never share a buffer
A reliable control plane for state that must never be lost, a lossy binary presence plane (8-byte position frames, a 15 Hz server tick, area-of-interest culling and delta snapshots) and a batched reaction plane. Lossy traffic is dropped under backpressure; control traffic never is. Per-event server work is bounded by the tick rate, not by how hard clients send.
- 02
Mini-games with no frames on the wire
All twelve attractions (chess, 8-ball, bowling, pinball, go-karts, rides, a synced cinema...) are pure, deterministic step(state, inputs) functions shared by browser and server. Only tick-stamped inputs travel; a late joiner's whole state can be as small as 54 bytes. As a bonus, the sims are tested like a library, and the tests caught real bugs.
- 03
Media bytes never touch the server
Audio stems are content-addressed (SHA-256) and resolve from a local IndexedDB cache, then Cloudflare R2 at zero egress. A shared transport clock keeps every client's playback aligned. I first built a full WebRTC peer-to-peer layer for this, then deleted it: NAT traversal rarely worked on mobile networks, it leaked peer IPs, and R2 already made it free.
- 04
A browser audio engine from scratch
A multi-stem mixer with Ableton-style auto-warp, pitch-preserving WSOLA time-stretch in Web Workers, an AudioWorklet wavetable synth with an FX bus, and a beat-grid arrangement DAW with waveform clips, a piano roll and collaborative editing.
- 05
Production, security and operations end to end
Cloudflare Pages plus a ~68-route Worker API (D1, KV, R2), a multi-VPS relay fleet with drain-based rolling deploys and client auto-update, Google OAuth, CSP/HSTS, an admin console behind Cloudflare Access with an audit log, and end-to-end encrypted DMs with a hybrid post-quantum scheme (X25519 + ML-KEM-768). I also wrote the threat model and the EN/TR GDPR–KVKK legal set.
Results
- 480
- concurrent players on a €4/month VPS
- ~2 ms
- worst server tick at full load (budget: 66 ms)
- 0
- dropped connections in load tests
- ~53k
- lines of strict TypeScript, solo
- 418
- automated tests
- 900 → 12
- draw calls after batching the scenery
Screenshots
What I learned
- Decide which traffic may be dropped and which may never be, and give them different pipes.
- Put the simulation in a pure function and test it like a library. Netcode bugs are mostly sim bugs with latency on top.
- Measure on the real box through the real path before buying a bigger one. My "scale" problem turned out to be a room-cap setting.
- Be willing to delete the part you're proudest of. Removing the P2P layer made the product better.