CASE · 052025 — ONGOINGSOLOEDGE · MULTIPLAYER
◦ DEFORBIT · deforbit.com

Co-op arcade. One URL. Zero origin.

Browser-2D-Shooter mit Waves, Bossen, Campaign und globalem Leaderboard — komplett auf Cloudflare-Edge. Kein Download. Kein Account. Kein Ad-Roll.

§ 01Problem · motivation

Why this exists.

Browser-Arcade ohne Ad-Rolls, ohne Account-Zwang, ohne Store-Umweg.

Browser-Arcade-Shooter sind heute entweder werbefinanzierte Portal-Spiele (Poki, Crazy Games) mit 2-Sekunden-Ad-Rolls vor jedem Run — oder native Mobile-Apps, die einen Store-Umweg und Account-Registrierung voraussetzen.

DefOrbit liefert ein vollwertiges 2D-Arcade-Erlebnis — Waves, Bosse, Co-Op, Campaign-Progression, globales Leaderboard — direkt aus einer einzigen URL. Kein Download, keine Ads, kein Account-Zwang für den Solo-Run. Zielgruppe: Gelegenheitsspieler, die auf Desktop oder Mobile „in den Pausenmodus" wollen.

§ 02Constraints · operating box

The box it had to fit in.

Free-Tier, Solo, Zero-Ops. Architektur musste sich dem unterordnen.
C/01 · OPS
Solo-Dev, kein Team — alles mit einem Befehl deploybar, keinerlei Ops-Overhead.
C/02 · BUDGET
Cap: ~0 €/Monat im Normalbetrieb. Muss in CF Free-Tiers (Pages, Workers, D1, DO) bleiben.
C/03 · BUILD
Zero-Build-Frontend — statisches index.html + ESM aus CDN. Kein Webpack, kein Vite.
C/04 · REALTIME
60 Hz lokal, Co-Op mit <100 ms perceived lag — ohne eigenen Spielserver.
C/05 · MOBILE
Touch-Steuerung, 60 fps auf iPhone-Safari, PWA-installierbar, Landscape + Offline (Leaderboard ausgenommen).
C/06 · TRAFFIC
Spiky + meist null — Rooms stehen minutenlang leer, einzelne Viral-Peaks. Scaling muss auf null fallen.
C/07 · ANTI-CHEAT
Hobby-Niveau: muss Konsolen-Injections und Replay abdecken — ohne DRM-Server.
C/08 · AUTH
Solo ohne Login. Campaign-Progression nur mit Google-OAuth — kein eigener Passwort-Stack.
§ 03Architecture · system topology

How it runs.

Zero-origin Cloudflare setup: Pages · Workers · D1 · Durable Objects — alles hinter einer Domain.
topology.live·ws ok·rtt
rooms 0·players 0·msgs/s 0
PAGES · 01
index.html · static
edge cache · 0 origin
cache hit99%
CLIENT · A · 02
Canvas · Three.js
ESM · bundler-free · 60fps
fps60
CLIENT · B · 03
Canvas · Three.js
co-op · same seed
fps60
PWA · 04
Service Worker
install · offline · landscape
scopedeforbit.com
SEED · 05
Mulberry32 RNG
SeededRNG.js · deterministic
scopeper-room
DO · 06
GameRoom · WS relay
stateful · hibernation
LOCKSTEP · 07
_inputDelay = 3
tick-aligned · 30s reconnect
inputs3-tick
CHECKSUM · 08
desync detection
score · enemies · earthHp
WORKER · 09
/api/* · scores · auth
JWT · rate-limit · campaign
req/s0
OAUTH · 10
Google · inline JWT
crypto.subtle · JWKS 1h
verifystateless
ANTI-CHEAT · 11
SHA-256 + SERVER_SALT
5m window · per-mode p99
nonceco-op skip
D1 · 12
SQLite · users · scores
campaign · earn_nonces
rows0
EVENT LOG · /api/* · DO messages · D1 writes
21:08:42doroom MX-92 · ws relay · 2 clients · tick 4812
21:08:41lockstepinput applied · _inputDelay=3 · seq 4815
21:08:39workerPOST /api/scores · jwt ok · 24ms
21:08:37anticheatsha256 ok · mode=solo · within p99 · accepted
21:08:34checksumdesync 0 · score 14820 · enemies 12 · earthHp 87
21:08:32clientA reconnect · 4.2s · resync from DO snapshot
21:08:29oauthjwks cache hit · google · sub:1108… · verified
21:08:26dohibernate · room MX-71 · idle 14m · state serialized
21:08:22d1campaign.purchase · WHERE currency>=cost · 1 row
21:08:42doroom MX-92 · ws relay · 2 clients · tick 4812
21:08:41lockstepinput applied · _inputDelay=3 · seq 4815
21:08:39workerPOST /api/scores · jwt ok · 24ms
21:08:37anticheatsha256 ok · mode=solo · within p99 · accepted
21:08:34checksumdesync 0 · score 14820 · enemies 12 · earthHp 87
21:08:32clientA reconnect · 4.2s · resync from DO snapshot
21:08:29oauthjwks cache hit · google · sub:1108… · verified
21:08:26dohibernate · room MX-71 · idle 14m · state serialized
21:08:22d1campaign.purchase · WHERE currency>=cost · 1 row
§ 04Decisions · trade-offs

Four deliberate choices.

Edge-native nicht als Buzzword, sondern als Kostenentscheidung.
D/01

Durable Object pro Room, statt Redis + Socket-Server.

chosen
Je Co-Op-Room eine GameRoom-DO-Instanz (Room-Code als stable ID). WebSocket direkt am DO.
instead of
Redis Pub/Sub + eigener Node-Socket-Server auf VPS
reason
Zero-Origin — ein wrangler deploy. DO-Hibernation hält Idle-Rooms bei ~0 Kosten. State (Spielerliste, Seed, Started-Flag, Disconnect-Timer) lebt im DO-Memory — kein externer KV. Auto-Expire nach 15 min idle.
D/02

Lockstep mit Seeded-RNG, statt authoritativem Server.

chosen
Server (DO) relayed nur Inputs + Checksums. Beide Clients simulieren mit identischem Mulberry32-Seed (SeededRNG.js) parallel.
instead of
Authoritativer Server führt Simulation, Clients rendern nur
reason
Spart Compute im DO (Free-Tier-freundlich), erlaubt trotzdem Desync-Detection (periodischer Checksum-Vergleich von score / enemies / earthHp) und 30-s-Reconnect-Window.
D/03

Static Frontend + Worker/D1, statt SPA + Backend.

chosen
index.html als Root von CF Pages. Worker hängt nur unter /api/* an derselben Domain.
instead of
Next.js oder SvelteKit mit API-Routes
reason
No-build Deployment, gemeinsame Origin erspart CORS in Produktion. D1 reicht für Scores / Users / Campaign (< 1 GB auf absehbare Zeit). Ein wrangler deploy + git push, alles ist live.
D/04

Google-OAuth inline verifizieren, statt OAuth-Library.

chosen
auth.js verifiziert Google-JWTs manuell via crypto.subtle.verify + JWKS-Cache (1 h TTL).
instead of
Passport, NextAuth oder Library aus npm
reason
Keine npm-Dependencies im Worker-Bundle (Size-Limit). Kein Session-Store nötig — Client schickt Token bei jedem Campaign-Call, Server validiert stateless.
§ 05Highlights · interesting bits

Things that were not obvious.

Netzwerk, Cheating, Rendering — die Details, die erst beim Bauen klar wurden.

Reconnect-Logik mit Tick-Delay

H/01
Das Co-Op-Netzwerkmodell nutzt _inputDelay = 3 Ticks — kein Client darf seinen eigenen Input instant anwenden. Er schickt ihn, beide Seiten applyen frühestens in 3 Ticks.

Das glättet die Simulation und erlaubt dem DO, bei Disconnect den 30-s-Reconnect-Timer zu halten, ohne den Partner sofort rauszuwerfen. Erst wenn der Timer abläuft, triggert partner-left.

Anti-Cheat mit Mode-Scope

H/02
Score-Submit rechnet SHA-256(score:kills:time:...:mode + SERVER_SALT), Timestamp-Window 5 min.

Für Campaign-Purchases: WHERE currency >= cost-Guard im UPDATE gegen Double-Click / Multi-Tab-Races. earn_nonces verhindert Replay. Outlier-Detection (p99-basiert) ist per-Mode gescoped und für Co-Op komplett übersprungen — zwei Spieler sprengen den Solo-p99 natürlich, ohne dass das geflaggt werden darf.

Single Draw Call für 512 Bullets

H/03
Alle bis zu 512 Bullets aus sechs Waffen (Pulse, Scatter, Lance, Homing, Plasma-Burst, Ricochet) werden in einer InstancedBufferGeometry mit Line-Primitive gerendert. Per-Instance-Attribute (iOff, iRot, iOn) werden im Vertex-Shader zu rotierter Line-Segment-Position. Off-Bullets via z = -9999 aus dem Clip-Space geschoben statt echtem Culling.

Ergebnis: ein Draw-Call egal wie voll die Szene ist — erlaubt UnrealBloom + 4 Shader-Passes auch auf Mobile bei 60 fps (mit reduziertem Bloom-Radius).

Scaling auf null

H/04
Durable-Object-Hibernation ist der stille Held dieser Architektur: Ein leerer Room kostet nichts, eine Viral-Spike skaliert pro Room isoliert (DO-Instanz ist der Scaling-Unit).

Der DO entscheidet per Heartbeat, wann er hibernatet — State wird serialisiert, nächste Nachricht weckt ihn auf. Kein Cron-Job nötig, um leere Rooms zu killen. Auto-Expire nach 15 min idle ist reine Sicherheitsleine, nicht Kostenmechanismus.
§ 06Stack · in production

What's running.

Cloudflare end-to-end. Keine npm-Runtime-Dependencies im Worker-Bundle.
Vanilla JS · ESMHTML5 CanvasThree.jsInstanced RenderingUnrealBloom PassCloudflare PagesCloudflare WorkersDurable ObjectsD1WebSocketsMulberry32 RNGLockstep SimGoogle OAuth (JWT inline)PWA · Service Workercrypto.subtle
§ 07Reflection · takeaways

What I learned.

Edge-native mit einem Solo-Budget. Was ich nächstes Mal anders plane.

Lockstep ist billiger als man denkt.

Deterministische Simulation wirkt im ersten Moment nach hohem Implementierungs-Aufwand. In der Praxis war der Seeded-RNG + Checksum-Vergleich weniger Code als ein authoritativer Server inklusive Interpolation gewesen wäre — und die Free-Tier-Kompatibilität war nebenbei ein Bonus. Für asymmetrische Gameplay-Modi wäre der Trade-off anders.

Draw-Call-Budget zuerst, Shader danach.

Der Instinkt bei Bullet-Hell ist, an Shader-Kosten zu sparen. Falsche Front. 512 Bullets × 512 Draw-Calls killt Mobile sofort; ein Draw-Call + teure Fragment-Shader läuft flüssig. Nächstes Game plane ich die Instanced-Pipeline als allererstes Architektur-Element, nicht als spätere Optimierung.

◦ NEXT CASE · 06 / 11
ToolPrime
← alle Projekte