Building Orbit: Why Orbit, Why This Stack
TL;DR
Orbit started from a very specific constraint triangle: browser-first gameplay, real-time social features, and leaderboard integrity. The architecture you see now (vanilla JS client + Cloudflare Worker + Durable Object + D1 + Queues) was not picked because it was trendy; it was the smallest stack that could satisfy deterministic gameplay, low-latency presence, and operational simplicity without introducing backend fleet management. This post explains those constraints, the tradeoffs behind each foundational decision, and what I got wrong early.
The Problem I Was Actually Solving
Most side projects begin as “build a fun game.” Orbit did not. Orbit began as:
- Can I run a reactive arcade loop in the browser with no install friction?
- Can players feel they are in a shared world (presence/chat/spectate), not an isolated score screen?
- Can leaderboard data stay credible under obvious tampering attempts?
If any one of those three fails, the whole product degrades:
- No low friction means poor onboarding.
- No shared presence means weak retention.
- No score integrity means leaderboard trust collapses.
So the architecture had to optimize all three simultaneously.
Constraint Matrix (The Real One)
Product Constraints
- Open instantly from a URL.
- Work on desktop and mobile browsers.
- Preserve daily freshness through seeded content and modifiers.
Engineering Constraints
- Minimal moving parts for a solo-maintainable codebase.
- Deterministic behavior for map generation and replay semantics.
- Practical observability without managing servers.
Security/Integrity Constraints
- Score submission cannot rely on client trust.
- Admin controls must be isolated from player session logic.
- Input and naming paths need abuse controls.
Why The Frontend Is Vanilla JS
The client runtime lives in public/game.js and is intentionally framework-free. That was not anti-framework ideology. It was a latency and control decision.
Reasons:
- The game loop and render synchronization are easier to reason about directly than through abstraction layers.
- UI updates are tightly coupled to game state transitions; DOM caching and direct mutation reduce overhead and hidden behavior.
- For this project scope, introducing a framework would add tooling and mental overhead that did not unlock proportional value.
Tradeoff accepted:
- Fewer guardrails and conventions than component frameworks.
- More responsibility for keeping state boundaries explicit.
Why Cloudflare Worker Is The Backend Entry Point
worker.js acts as a unified runtime for API handling, static assets integration, and queue/event integrations. I needed a backend that:
- deploys fast,
- avoids VM/container orchestration,
- stays close to edge network behavior,
- and can host real-time + CRUD/admin APIs in one place.
Cloudflare Worker fit this because it pairs well with the rest of the stack primitives I needed (Durable Objects, D1, Queues).
Tradeoff accepted:
- Runtime-specific patterns and limits require discipline in code shape.
- You design to platform primitives instead of assuming generic Node server semantics.
Why Durable Object For Presence Instead Of DIY Stateless Fan-Out
Presence and live chat require a per-room/per-state authority that is consistent across connections. The PLAYER_PRESENCE Durable Object binding in wrangler.jsonc gives Orbit a place to keep live connection state and enforce guardrails like spectator caps.
What this solved:
- A deterministic authority for connection/session state.
- Controlled fan-out behavior for real-time updates.
- Cleaner separation between request-response APIs and continuous socket flows.
Tradeoff accepted:
- You must model state transitions explicitly and defensively.
- Capacity limits and fan-out costs become first-class design concerns from day one.
Why D1 (SQLite) For Persistent State
Orbit needs persistent entities but not massive relational complexity yet: leaderboard rows, daily settings, admin credentials, profanity terms, replay rows, and analytics summaries.
D1 gave me:
- SQL semantics for predictable querying,
- straightforward schema evolution via migration files,
- local/remote workflow that can be scripted (
setup/prepare-db.mjs).
Tradeoff accepted:
- Must be rigorous about migration compatibility and naming consistency.
- Need careful indexing and growth planning as replay data volume rises.
Why Queues Exist In V1
wrangler.jsonc defines producers/consumers for chat, replay, and login analytics. That separation lets request paths stay responsive while heavier persistence/enrichment runs async.
What this buys:
- Lower latency for score submit path.
- Better fault isolation when analytics logic changes.
- Cleaner “ingest now, enrich later” architecture.
Tradeoff accepted:
- Eventual consistency becomes part of the product behavior.
- More operational thinking around retries, dead-letter queues, and idempotence.
What I Got Wrong Early
Wrong Assumption 1: “I can defer anti-tamper until later”
Reality: if score trust is not designed early, retrofitting confidence later is painful. Replay grammar and deterministic validation had to become core architecture, not a patch.
Wrong Assumption 2: “Spectate can be screenshot streaming”
Reality: bandwidth and CPU costs explode quickly with fan-out. Vector-state streaming became necessary to keep spectate practical.
Wrong Assumption 3: “Caching is always a free win”
Reality: caching dynamic API data (especially leaderboard) can silently damage trust. Cache policy has to be endpoint-aware.
Security Baseline From Day 0
I drew a strict line between “fun feature velocity” and “minimum security hygiene.” Orbit started with:
- signed, time-limited cookie sessions,
- CSRF protections on sensitive endpoints,
- admin auth and lockout controls,
- input validation and profanity-aware name checks,
- replay-based score verification before leaderboard acceptance.
Without this baseline, every later feature (spectate, daily content, admin ops) would amplify risk.
What This Architecture Is Optimized For
- Fast iteration by one maintainer.
- Strong control over deterministic gameplay and score semantics.
- Real-time social layer that scales carefully, not infinitely.
- Operational clarity over maximal abstraction.
What It Is Not Optimized For
- Massive enterprise team workflows.
- Arbitrarily high spectator fan-out without redesign.
- Fully generalized game engine reuse.
This is a product architecture, not a platform architecture.
Key Artifacts For Readers
If you are following this series hands-on, inspect these first:
README.mdpackage.jsonwrangler.jsoncworker.jspublic/game.js
What I Would Change If Restarting Today
- Establish a formal ADR (Architecture Decision Record) file from day one.
- Add incident notes earlier for caching/spectate experiments.
- Introduce an explicit performance budget document sooner.
Conclusion
Day 01 established the foundation: constraints first, architecture second. Choosing deterministic gameplay and edge-native infrastructure early made every later decision clearer and reduced costly rewrites.