<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>The Ops Community ⚙️: spryo xu</title>
    <description>The latest articles on The Ops Community ⚙️ by spryo xu (@spryo_xu_166077e27039fe12).</description>
    <link>https://community.ops.io/spryo_xu_166077e27039fe12</link>
    <image>
      <url>https://community.ops.io/images/zAGRLMbMuOP0qHJOmAj-q6nvEf-opyLPJmGrVCbkaXk/rs:fill:90:90/g:sm/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL3Vz/ZXIvcHJvZmlsZV9p/bWFnZS80MzU1MC8w/MTk0ODdjNS0zNzBm/LTQxNzgtYTVkMi1k/YjVkYzFjZjA3MTgu/cG5n</url>
      <title>The Ops Community ⚙️: spryo xu</title>
      <link>https://community.ops.io/spryo_xu_166077e27039fe12</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://community.ops.io/feed/spryo_xu_166077e27039fe12"/>
    <language>en</language>
    <item>
      <title>Separating Gameplay from Leaderboard Infrastructure in a Browser Game</title>
      <dc:creator>spryo xu</dc:creator>
      <pubDate>Fri, 16 Oct 2026 08:43:45 +0000</pubDate>
      <link>https://community.ops.io/spryo_xu_166077e27039fe12/separating-gameplay-from-leaderboard-infrastructure-in-a-browser-game-n60</link>
      <guid>https://community.ops.io/spryo_xu_166077e27039fe12/separating-gameplay-from-leaderboard-infrastructure-in-a-browser-game-n60</guid>
      <description>&lt;p&gt;A browser game can run locally while still needing a backend for shared records. The interesting engineering question is deciding where those two parts should meet.&lt;/p&gt;

&lt;p&gt;I’m building &lt;a href="https://mojiblade.com/" rel="noopener noreferrer"&gt;MojiBlade&lt;/a&gt;, a browser game where players choose a character and a typeface, then launch that character into battles. The shape of the character is part of the game, which makes trying different combinations central to the experience.&lt;br&gt;
&lt;a href="https://mojiblade.com/" rel="noopener noreferrer"&gt;&lt;img src="https://community.ops.io/remoteimages/uploads/articles/z0m3c4jvcq27u0tc8yib.png" alt="MojiBlade gameplay: characters battling in the arena" width="1280" height="900"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The gameplay runs in the browser. Shared leaderboards introduce a separate set of concerns: player identity, score submissions, database operations, publication schedules, and failure handling.&lt;/p&gt;

&lt;p&gt;Here is how I have separated those responsibilities using Cloudflare Workers, D1, KV, and Durable Objects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the game loop in the browser
&lt;/h2&gt;

&lt;p&gt;Rendering, input handling, and combat do not need a server round trip for every action.&lt;/p&gt;

&lt;p&gt;The browser handles those interactions, while the backend handles shared records. Local storage also preserves browser-side state between visits.&lt;/p&gt;

&lt;p&gt;This creates an important distinction: completing a battle and synchronizing its record are separate events.&lt;/p&gt;

&lt;p&gt;A player can finish a local game even when a score upload needs to be retried. However, a locally saved record should not be presented as proof that the shared leaderboard has accepted it.&lt;/p&gt;

&lt;p&gt;For an interface, that means distinguishing local progress from synchronization status.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give public rankings a separate read path
&lt;/h2&gt;

&lt;p&gt;For the lifetime leaderboard, public requests read a published snapshot through a cache and KV path.&lt;/p&gt;

&lt;p&gt;The authoritative records live in D1. A publication process turns those records into a public top-100 snapshot.&lt;/p&gt;

&lt;p&gt;The flow is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The browser submits a record to the Worker API.&lt;/li&gt;
&lt;li&gt;The API checks player ownership and processes the submission.&lt;/li&gt;
&lt;li&gt;A scheduled publisher checks for leaderboard changes.&lt;/li&gt;
&lt;li&gt;The publisher writes an updated public snapshot to KV when needed.&lt;/li&gt;
&lt;li&gt;Public leaderboard requests read the snapshot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The deployment configuration schedules publication checks every ten minutes.&lt;/p&gt;

&lt;p&gt;That is a deliberate freshness tradeoff. A public leaderboard can lag behind an accepted submission, so it should not imply that every successful upload immediately changes the displayed ranking.&lt;/p&gt;

&lt;p&gt;This arrangement also makes public reads easier to reason about: opening a leaderboard does not need to trigger a new ranking query against D1.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use revisions to avoid unnecessary publication
&lt;/h2&gt;

&lt;p&gt;A scheduled task does not necessarily need to rebuild its output on every run.&lt;/p&gt;

&lt;p&gt;The lifetime publisher checks a revision value before rebuilding the snapshot. If the revision has not changed and there is no pending publication, it can return without publishing again.&lt;/p&gt;

&lt;p&gt;When a rebuild is necessary, the implementation reads the revision and ranking entries in the same D1 batch transaction. This keeps the snapshot’s version tied to the records used to construct it.&lt;/p&gt;

&lt;p&gt;There is another useful check after that: a database revision can change without changing the public top 100.&lt;/p&gt;

&lt;p&gt;If the resulting entries match the previous snapshot, the publisher can update its internal revision without rewriting the same public content to KV.&lt;/p&gt;

&lt;p&gt;These checks address different questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Has the underlying ranking state changed?&lt;/li&gt;
&lt;li&gt;Has the public output actually changed?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Separating those questions avoids treating every internal update as a reason to republish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make publication recoverable
&lt;/h2&gt;

&lt;p&gt;Publication crosses storage boundaries. Reading D1 successfully does not guarantee that the following KV write will succeed.&lt;/p&gt;

&lt;p&gt;The Durable Object publisher stores a pending candidate before attempting the KV write. If publication fails, that candidate remains available for a later attempt.&lt;/p&gt;

&lt;p&gt;After a successful write, the publisher records the published snapshot and clears the pending state.&lt;/p&gt;

&lt;p&gt;The publisher also coalesces overlapping refresh calls into one in-flight job. This matters because asynchronous storage operations can overlap: without coordination, an older job could finish after a newer one.&lt;/p&gt;

&lt;p&gt;The aim is a publication process that can recover from partial completion without losing track of what it was trying to publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the public data explicitly
&lt;/h2&gt;

&lt;p&gt;A leaderboard needs player-facing fields such as a display name, character, typeface, and score. It does not need authentication material.&lt;/p&gt;

&lt;p&gt;The snapshot validator checks the expected structure, limits the number of entries, rejects duplicate player IDs, and rejects entries containing token fields.&lt;/p&gt;

&lt;p&gt;That is separate from checking ownership on a score submission.&lt;/p&gt;

&lt;p&gt;It is also worth being precise about the security boundary: verifying that a submission belongs to a player identity does not independently verify that a browser-computed score was legitimately earned.&lt;/p&gt;

&lt;p&gt;Those are different problems and need different controls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the boundaries between components
&lt;/h2&gt;

&lt;p&gt;For this architecture, useful acceptance checks include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can a player finish a battle when synchronization is unavailable?&lt;/li&gt;
&lt;li&gt;Is an accepted submission actually stored?&lt;/li&gt;
&lt;li&gt;Can a logged-out visitor load the public ranking?&lt;/li&gt;
&lt;li&gt;Does failed publication leave a recoverable candidate?&lt;/li&gt;
&lt;li&gt;Does the public snapshot exclude authentication data?&lt;/li&gt;
&lt;li&gt;Does the interface avoid presenting an older snapshot as an immediate update?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A successful API response, a saved database record, and a visible public ranking are three separate pieces of evidence.&lt;/p&gt;

&lt;p&gt;For this project, separating them has been a useful way to reason about the backend. Gameplay, record submission, and public publication each have their own timing and failure conditions. Giving each a clear responsibility makes the whole system easier to inspect.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
