Dev.to WebDev 🛠 Dev 👁 0 📖 4 min read

FrameShift Arena: Building a Deterministic, Replay-Safe PvP Engine on the Free Tier

By Mohammed Abdul Raqueeb (@raqueeb) · 2026-11-05 FrameShift Arena is a full-stack PvP web game: players build squads of original shape-shifting combat units under a hard power budget, then battle other players turn by

By Mohammed Abdul Raqueeb (@raqueeb) · 2026-11-05

FrameShift Arena is a full-stack PvP web game: players build squads of original shape-shifting combat units under a hard power budget, then battle other players turn by turn. The game design is the fun part, but it isn't the hard part. The hard part is the constraint sitting underneath it: the server has to be the sole authority on what happened in a match, and a replay watched months later has to reproduce that match exactly, action for action.

This is a tour of how that constraint shaped the engine, the data model and the infrastructure — and what it cost to keep a project like this running entirely on free-tier infrastructure.

The hardest problem: a replay that never lies

In most PvP games, "the client is wrong, trust the server" is a slogan. In FrameShift Arena it's an architectural decision with one consequence: a battle is computed exactly once, by one pure function, and everything else — the live match, the spectator view, the replay someone opens six months from now — plays back the same recorded actions through that same function.

The engine itself is dependency-free TypeScript, versioned (currently v3.2.0), and shared verbatim between the API (where it's the authoritative resolver) and the SPA (where it's playback only). Nothing about battle outcomes is duplicated or re-derived client-side. When a match starts, the server snapshots both squads onto the match row and stamps the seed and the engine version used to resolve it. That's enough for a replay to keep reproducing its original result even after the engine is later rebalanced — the match carries the exact state and version it was fought under, not whatever the catalogue or formulas look like today.

Architecture: one write path, one source of truth

Every turn boils down to the same shape: the client sends {expectedVersion, action}, and the single Hono function running at api/index.ts resolves it against a conditional UPDATE ... WHERE version = ?. If the version has moved — someone else's action landed first — the write fails outright with a 409 and the client retries against the fresh state. If it succeeds, the action is appended to the match's event log.

 client  ──{expectedVersion, action}──▶  Hono /api (1 fn)
                                              │
                                  UPDATE … WHERE version = ?
                                              │
                      ┌─────────────────────┴─────────────────────┐
                      │                                            │
                  version ok                                 version moved
                      │                                            │
             event log: append                             409 → client retries
                      │
          SPA folds the log  ──▶  live playback  +  replay

   fig. 1 — turn resolution: one conditional write, 409 on conflict, replay by folding the log

The battle UI never runs its own battle math. It's a fold over that same persisted event log — one cursor mechanism drives both the live animation as new events arrive and the replay viewer scrubbing through old ones. Live play and replay are the same code path, not two features kept in sync by hand.

Fitting inside a free tier

Vercel's Hobby plan doesn't cap serverless functions outright. For Next.js and SvelteKit, dynamic routes get bundled automatically into the fewest functions possible. For everything else — including a plain Hono API like this one — every API file maps to its own function, and Hobby caps that at 12 Vercel Functions per deployment. That's a real constraint, not a soft guideline, and the obvious way to hit it is one route per feature. FrameShift Arena runs on exactly one: a single Hono router mounted at api/index.ts handles every endpoint, so the cap is structurally unhittable rather than something to remember not to break. A CI guard script enforces that invariant directly, instead of trusting code review to catch a second function sneaking in.

The rest of the stack follows the same discipline. Neon Postgres and Vercel both pause rather than bill when idle, which shapes decisions around cold starts and connection handling. The app also ships as a PWA with a service worker and an offline page, so a dropped connection mid-session degrades instead of just failing.

Testing at scale

2,096 tests across 104 files back the project, and the engine tests aren't just checking final outputs — they check the full event log against golden snapshots, so a change that alters how a battle resolves, not only its final result, gets caught. Every server test boots its own PGlite database rather than sharing fixtures or mocking the data layer, trading a slower test run for tests that exercise the real persistence path.

How much is actually built

By Phase 3 (milestones M19–M25, now complete), the scope reaches well past the battle engine itself: a 54-sector world, a frozen 3,063-species catalogue, bots and rogue operators to fight, live and asynchronous PvP, matchmaking, spectating, trading, auctions, clans, seasons and achievements. GitHub Actions CI runs structural guards, typecheck, lint, tests and build before any migration touches the database.

What I'd take into the next project

Putting all battle math in one pure module, with the UI as a fold over the event log, turned live play and replay from two features that had to be kept in sync into one code path that can't drift. And treating the Vercel Hobby plan's 12-function cap as a design constraint — enforced structurally, not just remembered — beat hoping never to hit it.

The source repository stays private — this write-up is as deep as I can go without walking through the code line by line. What's public is the build itself: frameshift-arena.vercel.app is live and playable right now, running the same engine and infrastructure described above. The project is designed to operate within the free tiers of its infrastructure providers, subject to their usage limits and conditions.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.