Co-op arcade. One URL. Zero origin.
Browser 2D shooter with waves, bosses, campaign and a global leaderboard — fully on the Cloudflare edge. No download. No account. No ad-roll.
Why this exists.
Browser arcade shooters today are either ad-funded portal games (Poki, Crazy Games) with two-second ad-rolls before every run — or native mobile apps that demand a store detour and account signup.
DefOrbit delivers a full 2D arcade experience — waves, bosses, co-op, campaign progression, global leaderboard — straight from a single URL. No download, no ads, no forced account for the solo run. Target audience: casual players who want to drop into "break mode" on desktop or mobile.
The box it had to fit in.
index.html + ESM from CDN. No Webpack, no Vite.How it runs.
Four deliberate choices.
Durable Object per room, instead of Redis + socket server.
GameRoom DO instance per co-op room (room code as stable ID). WebSocket straight on the DO.wrangler deploy. DO hibernation keeps idle rooms at ~0 cost. State (player list, seed, started flag, disconnect timer) lives in DO memory — no external KV. Auto-expire after 15 min idle.Lockstep with seeded RNG, instead of an authoritative server.
SeededRNG.js).score / enemies / earthHp) and a 30-second reconnect window.Static frontend + Worker/D1, instead of SPA + backend.
index.html as the root of CF Pages. The Worker attaches only under /api/* on the same domain.wrangler deploy + git push and everything is live.Verify Google OAuth inline, instead of an OAuth library.
auth.js verifies Google JWTs manually via crypto.subtle.verify + a JWKS cache (1 h TTL).Things that were not obvious.
Reconnect logic with tick delay
_inputDelay = 3 ticks — no client is allowed to apply its own input instantly. It sends the input, both sides apply it three ticks later at the earliest.That smooths the simulation and lets the DO hold the 30-second reconnect timer on a disconnect without kicking the partner out immediately. Only once the timer expires does
partner-left fire.Anti-cheat with mode scope
SHA-256(score:kills:time:...:mode + SERVER_SALT), timestamp window 5 min.For campaign purchases:
WHERE currency >= cost guard in the UPDATE to stop double-click / multi-tab races. earn_nonces prevents replay. Outlier detection (p99-based) is per-mode scoped and skipped entirely for co-op — two players naturally blow past the solo p99 and shouldn't get flagged for it.Single draw call for 512 bullets
InstancedBufferGeometry with a line primitive. Per-instance attributes (iOff, iRot, iOn) become rotated line-segment positions in the vertex shader. Off bullets get pushed out of clip space via z = -9999 rather than real culling.Result: one draw call no matter how full the scene is — lets UnrealBloom + 4 shader passes still hit 60 fps on mobile (with a reduced bloom radius).
Scaling to zero
The DO decides via heartbeat when to hibernate — state gets serialized, the next message wakes it back up. No cron job needed to kill empty rooms. Auto-expire after 15 min idle is a pure safety net, not a cost mechanism.
What's running.
What I learned.
Lockstep is cheaper than you'd think.
Deterministic simulation sounds like a lot of implementation work at first. In practice, seeded RNG + checksum comparison was less code than an authoritative server including interpolation would have been — and free-tier compatibility was a nice bonus on the side. For asymmetric gameplay modes the trade-off would look different.
Draw-call budget first, shaders after.
The instinct with bullet hell is to cut shader cost. Wrong front. 512 bullets × 512 draw calls kills mobile instantly; one draw call + expensive fragment shaders runs smooth. Next game I'll plan the instanced pipeline as the very first architectural element, not as a later optimization.