← back

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.md
  • package.json
  • wrangler.jsonc
  • worker.js
  • public/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.