<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>Cloudflare - Tag - SaiyuDoc</title><link>https://www.wangsaiyu.com/tags/cloudflare/</link><description>Cloudflare - Tag - SaiyuDoc</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><managingEditor>panicwang@outlook.com (王赛宇)</managingEditor><webMaster>panicwang@outlook.com (王赛宇)</webMaster><lastBuildDate>Wed, 14 Oct 2026 14:00:00 +0800</lastBuildDate><atom:link href="https://www.wangsaiyu.com/tags/cloudflare/" rel="self" type="application/rss+xml"/><item><title>游戏后台选型：从 Supabase 到 Cloudflare Workers 与 Durable Objects</title><link>https://www.wangsaiyu.com/posts/engineering/game-backend-supabase-cloudflare-do/</link><pubDate>Wed, 14 Oct 2026 14:00:00 +0800</pubDate><author>王赛宇</author><guid>https://www.wangsaiyu.com/posts/engineering/game-backend-supabase-cloudflare-do/</guid><description><![CDATA[<p>在 Combos，Agent 生成的 H5 游戏最初主要在浏览器里运行。要增加排行榜、云端存档和多人房间，就需要给每个游戏准备一个能够保存数据、交换消息的后台。我们在 2026 年 4 月接入 Supabase，9 月开始把新游戏的联机方案切换到 Cloudflare Workers 与 Durable Objects，后文简称 Worker 和 DO。</p>
<p>这次选型涉及两个阶段的目标：早期先让 Agent 能够开通后端、使用数据和联机能力；后来，实时对局需要一个稳定的服务端来执行规则，大量低频访问的游戏也开始暴露实例启停与闲置成本问题。下面结合当时的讨论、代码变化和目前的实现，分别介绍两套方案如何工作，以及我们做了哪些取舍。</p>
<h2 id="1-我们需要什么样的游戏后台">1. 我们需要什么样的游戏后台</h2>
<p>这里讨论的是<strong>游戏生成平台为用户作品提供的运行后台</strong>。平台自己的账号、项目、计费、发布记录，仍然由原有业务服务管理。本文中的切换，主要发生在生成出来的游戏如何保存状态、组织联机、部署自己的逻辑这一层。</p>
<p>这个场景有几个特点。首先，游戏数量可以持续增长，但每个游戏的活跃程度差异很大。有的作品只在创作时玩过几次，有的隔几天才有人打开，也可能突然出现一批同时在线的玩家。我们既要考虑热门游戏的承载，也要考虑大量闲置作品留下的资源。</p>
<p>其次，各个游戏的数据结构并不相同。一个解谜游戏只需要存关卡进度，一个竞速游戏需要排行榜，一个合作游戏还需要房间、成员和共享物品。Agent 需要根据玩法编写这些逻辑，平台很难提前为所有游戏规定同一组业务接口。</p>
<p>最后，“两个玩家能交换消息”和“两个玩家参与同一场可靠的对局”之间，还有一段距离。假设玩家 A 和 B 同时去捡一枚宝石，后台要确定谁先拿到、谁能加分；玩家断线重连之后，也要恢复已经确认的结果。负责决定这些结果的程序，称为<strong>权威服务</strong>。它必须拥有可信的状态，并按游戏规则接受或拒绝玩家的操作。</p>
<p>为了把后面的流程讲具体，本文用房间 <code>AB23CD</code> 中两名玩家争抢宝石作为示意。房间号和操作数据均为例子，不对应真实玩家或线上房间。</p>
<h2 id="2-最初为什么选择-supabase">2. 最初为什么选择 Supabase</h2>
<h3 id="21-先让-agent-获得可用的后台">2.1. 先让 Agent 获得可用的后台</h3>
<p>4 月 13 日的接入讨论中，最先明确的是：让创作流程能够开通 Supabase，并让 Agent 使用它。排行榜被当作验证这种能力的一个例子，具体规则由 Agent 根据游戏需求实现。第二天的讨论又把目标收窄到“有房间、能一起玩”，先跑通简单联机，再看更复杂玩法的需要。</p>
<p>对于这个阶段，Supabase 提供了一组已经组合好的能力：PostgreSQL 保存业务数据，Data API 把表操作暴露成接口，Realtime 提供消息广播、在线状态和数据库变更订阅，另外还有 Auth、Storage 和 Edge Functions 等组件。Agent 可以围绕常见 SDK、SQL 和浏览器代码完成一个有后台的游戏，平台先接好资源与权限入口。<a href="https://supabase.com/docs/guides/getting-started/architecture" target="_blank" rel="noopener noreffer ">Supabase 架构说明</a></p>
<p>这降低了第一阶段需要自建的范围。我们不必先做完一套通用房间服务器、数据接口生成器和实时消息服务，才能验证“生成的游戏能否拥有自己的在线数据”。</p>
<h3 id="22-游戏之间先按项目隔离">2.2. 游戏之间先按项目隔离</h3>
<p>早期方案采用“一款游戏项目对应一个独立的阿里云托管 Supabase 项目”。平台保存项目与实例的绑定、开通状态和访问所需的身份信息，游戏自己的房间、成员、事件和记录留在对应的 Supabase 中。</p>
<figure><a class="lightgallery" href="/images/game-backend-supabase-cloudflare-do/01-supabase-projects.svg" title="平台控制面分别管理游戏 A 和游戏 B 的独立 Supabase 项目，各游戏玩家访问各自的 PostgreSQL、Realtime 和 Storage" data-thumbnail="/images/game-backend-supabase-cloudflare-do/01-supabase-projects.svg" data-sub-html="<h2>图 1：早期按游戏项目隔离的 Supabase 方案</h2>">
        
    </a><figcaption class="image-caption">图 1：早期按游戏项目隔离的 Supabase 方案</figcaption>
    </figure>
<p>这样，游戏 A 增加一个背包表，不需要让游戏 B 配合修改。Agent 生成的表结构、权限策略和后续扩展，也可以限制在单个项目内。代价是项目越来越多时，实例生命周期和基础资源成本也会随之增长。</p>
<p>4 月落下来的代码包含三类工具：确保项目的 Supabase 运行环境存在、读取表结构、应用受限制的结构扩展。初始化模板准备了 <code>rooms</code>、<code>room_members</code>、<code>room_events</code>、<code>game_state_snapshots</code>、<code>leaderboard_entries</code> 等表，同时配置索引、权限策略和 Realtime publication。平台由此能给 Agent 一份有明确起点的运行环境。</p>
<p>这个设计也划清了数据归属：平台管理资源，游戏后台管理游戏状态。平台的中心数据库没有必要再保存另一份随时变化的房间快照，否则一次操作究竟以哪份状态为准，又会成为新的问题。</p>
<h3 id="23-已有身份可以接到数据权限上">2.3. 已有身份可以接到数据权限上</h3>
<p>游戏并不需要因为使用 Supabase，就再让玩家注册一遍账号。早期实现会把平台识别出的玩家映射成 Supabase 可用的身份，签发短期 JWT，使数据库里的 <code>auth.uid()</code> 能识别调用者；RLS，也就是行级安全策略，再判断这个玩家能读取或修改哪些记录。</p>
<p>例如个人存档可以要求记录中的玩家标识与当前身份相同，房间数据可以要求调用者已经加入对应房间。高权限密钥保留在服务端，浏览器拿到的是受限制的访问能力。这是身份与数据授权的组合方式。<a href="https://supabase.com/docs/guides/database/postgres/row-level-security" target="_blank" rel="noopener noreffer ">Supabase RLS</a></p>
<p>RLS 解决的是“谁能访问这条记录”。要判断一次拾取是否合法、一个分数是否可信，仍然需要业务规则。即使一名玩家只能修改自己的排行榜记录，也不能因此允许他把分数任意改成一百万。</p>
<h3 id="24-为什么落到阿里云托管版本">2.4. 为什么落到阿里云托管版本</h3>
<p>当时的讨论给出了一个很具体的原因：希望使用自动启停，减少低频项目持续占用计算资源的费用。4 月 13 日，这还是选型中的预期；4 月 16 日，团队继续与供应商确认功能时间、闲置阈值以及唤醒耗时，4 月 23 日的代码再接入自动启停调用。</p>
<p>因此，本文涉及的实例规格、创建耗时、休眠行为和兼容问题，首先对应我们使用的<strong>阿里云托管 Supabase</strong>。它们与 Supabase 的开源组件、官方托管产品、其他自建部署之间，需要分别判断。</p>
<h2 id="3-从接入到切换的时间线">3. 从接入到切换的时间线</h2>
<p>下面的日期均为 2026 年，按北京时间整理，表内用“月/日”表示。讨论记录用于还原当时的目标和判断；代码记录用于确认实现变化。合并日期不直接代表全部线上资源已经迁移。</p>
<table>
  <thead>
      <tr>
          <th>时间</th>
          <th>当时发生的变化</th>
          <th>对选型的含义</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>4/13—14</td>
          <td>确定在创作流程中开通 Supabase，用排行榜和简单联机验证；同时讨论高频对战需要权威服务</td>
          <td>初期目标是尽快具备后台能力，实时计算的边界当时已经被意识到</td>
      </tr>
      <tr>
          <td>4/16</td>
          <td>交流自动启停细节，记录到的停机后唤醒预期约为一分钟</td>
          <td>降低闲置费用与首次进入体验之间存在取舍</td>
      </tr>
      <tr>
          <td>4/22—30</td>
          <td>接入项目级运行环境工具、结构初始化与玩家身份；接入自动启停；默认生成路径转向 Direct Realtime 并补充指引</td>
          <td>Supabase 从选型进入可供 Agent 使用的工程链路</td>
      </tr>
      <tr>
          <td>5/14</td>
          <td>加强联机发布检查和运行时配置约束</td>
          <td>后台已经开通，仍需要检查游戏是否真正接上</td>
      </tr>
      <tr>
          <td>9/9</td>
          <td>Supabase CLI 扩展到账号级资源与访问会话；同时修复托管实例上的 Realtime 协议兼容问题</td>
          <td>此时仍在完善 Supabase，切换是后续持续评估的结果</td>
      </tr>
      <tr>
          <td>9/9—11</td>
          <td>讨论专门的房间服务；9 月 11 日明确指出房主裁决导致额外转发，并开始切换 DO</td>
          <td>决策焦点转到权威逻辑的位置和实时链路</td>
      </tr>
      <tr>
          <td>9/11—12</td>
          <td>Worker／DO 管理与 CLI 接入，随后修正初始 Worker 创建和 DO 绑定</td>
          <td>新方案开始形成可部署、可继续更新的后台资源</td>
      </tr>
      <tr>
          <td>9/14—18</td>
          <td>交流静态游戏、轻后台和实时对战的统一承载；9 月 16 日停用 Agent 的 Supabase Tool 和 Skill；9 月 18 日向游戏制作团队介绍新能力</td>
          <td>新游戏生成路径发生切换，产品侧开始使用和适配</td>
      </tr>
      <tr>
          <td>9/18—22</td>
          <td>继续处理存量 Supabase 闲置费用；联机专项提出快速入局、掉线恢复、服务端裁决等要求</td>
          <td>旧资源维护与新玩法推进同时进行</td>
      </tr>
      <tr>
          <td>9/29—10/13</td>
          <td>补充固定步长房间、快照／增量与恢复示例，再修正内置 CLI 和实际加载的 Worker Skill</td>
          <td>新方案仍在完善生成质量和工具使用链路</td>
      </tr>
  </tbody>
</table>
<p>这里有一个容易被倒推错的顺序：9 月中下旬对“快速进入持续运行的对局”的要求，进一步强化了新方案的价值；9 月 11 日开始切换时，已经有更早的实时链路问题。不能把后来的全部产品规范，都写成最初决策时已经明确的要求。</p>
<h2 id="4-supabase-方案后来遇到了什么">4. Supabase 方案后来遇到了什么</h2>
<h3 id="41-消息广播与游戏裁决分在两处">4.1. 消息广播与游戏裁决分在两处</h3>
<p>Supabase Realtime 的几种能力需要先分清：</p>
<ul>
<li><strong>Broadcast</strong> 把一个客户端或服务端发送的消息分发给频道中的其他订阅者，客户端广播可以直接经过 WebSocket，不要求每条消息先写数据库。</li>
<li><strong>Presence</strong> 维护频道成员上报的在线状态，适合表达谁在房间中。</li>
<li><strong>Postgres Changes</strong> 把数据库里已经发生的记录变化通知给订阅者，适合让界面跟随持久数据更新。</li>
</ul>
<p>这三件事分别帮助消息传递、在线协作和数据订阅。它们不会自动知道宝石属于谁，也不会自动运行某款游戏的碰撞检测。<a href="https://supabase.com/docs/guides/realtime/broadcast" target="_blank" rel="noopener noreffer ">Broadcast</a>、<a href="https://supabase.com/docs/guides/realtime/presence" target="_blank" rel="noopener noreffer ">Presence</a>、<a href="https://supabase.com/docs/guides/realtime/postgres-changes" target="_blank" rel="noopener noreffer ">Postgres Changes</a></p>
<p>我们当时的一类联机游戏把权威逻辑放在房主客户端。玩家 A 发出“拾取宝石”的操作，消息先通过 Supabase 到达房主；房主根据自己的状态算出结果，再通过 Supabase 发给其他玩家。9 月 11 日的群内说明，明确把这种路径列为旧联机游戏卡顿的原因之一，并提出把权威服务放到 DO。</p>
<figure><a class="lightgallery" href="/images/game-backend-supabase-cloudflare-do/02-authority-path.svg" title="旧实现由 Supabase 将玩家操作转发给房主，再由房主经 Supabase 广播结果；新实现让 Durable Object 执行裁决并通知玩家" data-thumbnail="/images/game-backend-supabase-cloudflare-do/02-authority-path.svg" data-sub-html="<h2>图 2：把权威逻辑从房主客户端移到 DO；箭头表示逻辑路径，不表示实测耗时</h2>">
        
    </a><figcaption class="image-caption">图 2：把权威逻辑从房主客户端移到 DO；箭头表示逻辑路径，不表示实测耗时</figcaption>
    </figure>
<p>这条旧路径把房主的网络和浏览器也放进了关键链路。房主切到后台、刷新页面或者离开房间，都可能影响裁决的连续性。若要继续维持客户端房主，还得处理房主选举、状态交接和重复操作。</p>
<p>搬到 DO 以后，房间中的玩家都把意图交给同一个服务端对象处理。裁决不再依赖某个玩家的设备，结果也不必为了完成裁决而绕到房主再回来。这是从调用路径能够解释的改进；现有资料没有给出相同网络、相同玩法下完整的迁移前后延迟分布，因此这里不补写“快了几倍”或统一的毫秒数。</p>
<p>Supabase 也可以配合独立的权威房间服务使用。9 月 9 日我们就讨论过 Colyseus 和独立计算资源。那条路会保留 Supabase 的数据能力，再增加一个运行游戏规则的服务。选择 DO，则是希望把按房间执行的计算、连接和状态放进同一个托管单元。</p>
<h3 id="42-数据库变更通知不能替代所有实时消息">4.2. 数据库变更通知不能替代所有实时消息</h3>
<p>如果把每次移动都实现成“更新数据库记录，再等订阅通知”，每个输入都会带来持久化和变更分发的工作。对于关卡存档和最终结算，这样的代价有明确用途；对于下一帧就会被覆盖的位置，它未必值得。</p>
<p>Supabase 官方也把 Broadcast 与 Postgres Changes 分开讨论：Postgres Changes 需要按订阅者执行权限检查，并为保序串行处理变更，订阅规模增加时存在相应开销；当前文档建议多数数据库事件分发场景优先考虑 Broadcast。<a href="https://supabase.com/docs/guides/realtime/subscribing-to-database-changes" target="_blank" rel="noopener noreffer ">数据库变更订阅方式</a></p>
<p>因此，不能把旧路径的额外转发全部归因于“每次广播都要落库”。我们需要同时识别两个问题：游戏采用了哪一种消息机制，以及真正执行规则的程序在哪里。</p>
<h3 id="43-自动启停仍有闲置窗口和唤醒等待">4.3. 自动启停仍有闲置窗口和唤醒等待</h3>
<p>4 月 16 日的会议反馈记录，停机后新流量到来，实例恢复预计约需一分钟，请求在恢复期间等待。此次核对时，阿里云自动启停文档也给出了约一分钟的唤醒说明，并明确暂停后仍收取存储费用。这个口径属于所讨论的托管方案，实际体验还受实例、网关和客户端超时设置影响。<a href="https://help.aliyun.com/zh/analyticdb/analyticdb-for-postgresql/supabase-auto-start-stop" target="_blank" rel="noopener noreffer ">阿里云 Supabase 自动启停</a></p>
<p>对一款偶尔有人打开的小游戏，这会形成两难：保持运行，闲置时也要支付计算费用；积极休眠，下一位玩家进入时又可能等待较久。后来联机专项提出的快速入局目标，使这类等待更难隐藏在产品流程里。</p>
<p>闲置阈值也会影响成本。用户玩了两分钟，不等于实例只产生两分钟计算费用：离开后的等待窗口、后续探测或其他请求，都可能延长实例的运行时间。9 月的成本协作仍在追查哪些实例被保活，以及长期闲置存储如何处理。</p>
<p>还要区分<strong>首次创建实例</strong>和<strong>已有实例从休眠恢复</strong>。早期资源开通代码给首次创建预留了 25 分钟轮询预算；这表示控制面需要容纳异步创建过程，不表示每次玩家进入都要等待这个时长，也不能拿它与一次热请求直接比较。</p>
<h3 id="44-一次具体的兼容问题">4.4. 一次具体的兼容问题</h3>
<p>9 月 9 日的仓库记录提供了一个很有代表性的失败样本。使用 <code>@supabase/supabase-js 2.115.0</code> 连接当时测试的阿里云托管实例时，两个独立客户端都能加入频道，但默认协议下 Broadcast 的确认超时，双方收不到对方的消息。显式配置 <code>realtime.vsn: '1.0.0'</code> 后，双向广播和 Presence 的加入、离开检查通过。</p>
<p>这里的 <code>1.0.0</code> 是 Realtime 线协议版本，SDK 仍然使用 V2。该记录覆盖了供应商直连和平台代理两条路径上的独立 Node 客户端测试，没有据此确认服务端组件的根因，也没有覆盖全部浏览器游戏。官方协议文档说明了两个线协议版本的帧格式差异，可用于理解为什么加入频道成功仍不足以确认广播兼容。<a href="https://supabase.com/docs/guides/realtime/protocol" target="_blank" rel="noopener noreffer ">Realtime 协议</a></p>
<p>这个问题后来得到兼容处理。它说明托管组件、SDK 和平台代理之间需要实际联调，不能用“连接已经建立”替代“两名玩家真的收到了同一条消息”。切换到 Worker／DO 后，这种端到端验证也仍然需要。</p>
<h2 id="5-worker-与-do-如何组成游戏后台">5. Worker 与 DO 如何组成游戏后台</h2>
<h3 id="51-worker-负责入口do-负责一组共享状态">5.1. Worker 负责入口，DO 负责一组共享状态</h3>
<p>Worker 接收 HTTP 请求、检查请求形状、处理路由，再把属于某个房间的请求交给 DO。一个 DO 有稳定的身份、自己的内存和附属持久存储，可以集中处理需要共享同一份状态的客户端。<a href="https://developers.cloudflare.com/durable-objects/concepts/what-are-durable-objects/" target="_blank" rel="noopener noreffer ">Durable Objects 的对象模型</a></p>
<p>我们通常让一款游戏拥有自己的 Worker，再在它的 DO 类命名空间里，按房间号寻找房间对象。当前房间示例中的路由核心可以写成下面这样，入口还需要在此前完成房间号与访问权限检查：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="c1">// 示意：相同命名空间内，相同房间号路由到同一个对象。
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">roomId</span> <span class="o">=</span> <span class="s2">&#34;AB23CD&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">id</span> <span class="o">=</span> <span class="nx">env</span><span class="p">.</span><span class="nx">ROOM</span><span class="p">.</span><span class="nx">idFromName</span><span class="p">(</span><span class="nx">roomId</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">room</span> <span class="o">=</span> <span class="nx">env</span><span class="p">.</span><span class="nx">ROOM</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">id</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="k">return</span> <span class="nx">room</span><span class="p">.</span><span class="nx">fetch</span><span class="p">(</span><span class="nx">request</span><span class="p">);</span>
</span></span></code></pre></div><p>房间 <code>AB23CD</code> 的 A、B 两名玩家因此会到达同一个对象。另一个房间使用另一个对象，不需要和它共享当前对局的内存。这里的隔离层次也更细了：Worker 对应一份游戏后端代码，DO 实例对应一组需要共同裁决的状态。</p>
<h3 id="52-一次拾取如何执行">5.2. 一次拾取如何执行</h3>
<p>下面是应用应该实现的处理过程，用来说明权威房间的作用，并非平台替所有游戏预置的宝石玩法：</p>
<ol>
<li>A 发送操作 <code>pick-42</code>，表达“我想拾取宝石 7”，附上自己的输入序号。</li>
<li>房间检查 A 的身份、操作是否重复、宝石是否还存在，以及距离、冷却等游戏规则。</li>
<li>房间接受这次操作，更新宝石归属和 A 的分数；需要在重启后保留的结果，在确认成功前完成持久化。</li>
<li>房间把已确认结果发送给 A、B。B 随后也请求拾取宝石 7 时，服务器已经能看见它被取走，便拒绝或返回当前状态。</li>
</ol>
<p>客户端可以立即播放伸手、移动等反馈，但最终归属和得分以服务端结果为准。这样的分工既保留操作手感，也避免每个客户端各自决定一个互相冲突的结局。</p>
<p>DO 把这些状态集中到一个执行位置，减少了应用自己寻找“当前房主”的工作。不过 JavaScript 的异步处理仍可能交错：在读取状态后 <code>await</code> 一个外部请求，期间其他事件可能已经改变状态。事务、幂等记录以及异步调用前后的检查，仍需要按业务设计。<a href="https://developers.cloudflare.com/durable-objects/best-practices/rules-of-durable-objects/" target="_blank" rel="noopener noreffer ">DO 并发规则</a></p>
<h3 id="53-平台管理与玩家运行分开">5.3. 平台管理与玩家运行分开</h3>
<p>目前的管理链路由平台服务接收 CLI 的创建、部署、查询、日志和删除请求，再调用 Cloudflare。游戏运行时，浏览器直接连接该游戏的 HTTP／WSS 地址，通过分发入口到达它的 Worker 和 DO。</p>
<figure><a class="lightgallery" href="/images/game-backend-supabase-cloudflare-do/03-worker-runtime.svg" title="Agent 经平台资源管理服务部署 Worker；玩家流量经 Dispatcher 到游戏 Worker，再按房间号进入不同 Durable Object" data-thumbnail="/images/game-backend-supabase-cloudflare-do/03-worker-runtime.svg" data-sub-html="<h2>图 3：管理链路部署后端代码，运行链路处理玩家请求</h2>">
        
    </a><figcaption class="image-caption">图 3：管理链路部署后端代码，运行链路处理玩家请求</figcaption>
    </figure>
<p>分发入口使用 Workers for Platforms 的 dispatch namespace。它先根据访问地址找到对应游戏的 Worker，游戏 Worker 再选择房间对象。这适合一个平台管理很多用户作品的形态。<a href="https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/" target="_blank" rel="noopener noreffer ">Workers for Platforms</a></p>
<p>9 月中旬，我们还讨论过把纯静态游戏、静态游戏加轻后台、实时对战加存档排行榜，放在更统一的承载方式下。这是当时的扩展方向。当前核对到的 CLI 文档仍把 Worker 部署与 H5 发布列为两个动作，不能据此声称静态发布链路已经全部迁到 Cloudflare。</p>
<h2 id="6-do-的持久化和休眠">6. DO 的持久化和休眠</h2>
<h3 id="61-内存连接附件和存储各放什么">6.1. 内存、连接附件和存储各放什么</h3>
<p>一个房间运行起来后，至少要区分三类状态：</p>
<table>
  <thead>
      <tr>
          <th>位置</th>
          <th>适合保存的内容</th>
          <th>恢复边界</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DO 进程内存</td>
          <td>当前模拟状态、输入队列、临时索引</td>
          <td>休眠或重启后需要重建</td>
      </tr>
      <tr>
          <td>WebSocket 连接附件</td>
          <td>玩家与连接的关联等少量元数据</td>
          <td>可以跨 Hibernation 保留，但不能替代长期存档</td>
      </tr>
      <tr>
          <td>DO 持久存储</td>
          <td>房间快照、已确认结果、需要持久去重的记录</td>
          <td>重新加载同一个对象时用于恢复</td>
      </tr>
  </tbody>
</table>
<p>当前平台为新 DO 类配置 SQLite 存储。对象内部可以使用 SQL，也可以通过对象存储的键值接口保存快照；这些数据属于这个对象。这里的键值接口与 Cloudflare 的独立产品 Workers KV 需要区分，本文的房间数据使用的是 DO 自己的存储。<a href="https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/" target="_blank" rel="noopener noreffer ">DO SQLite 存储 API</a></p>
<p>存储跟着对象并不意味着任意内存变量都会自动保存。一次网络发送，也不能替代一次持久写入。对于可丢弃的位置采样，可以接受下一帧覆盖；对于已宣布成功的结算或奖励，则需要先确保对应记录可靠保存。</p>
<h3 id="62-休眠保持连接恢复仍要写代码">6.2. 休眠保持连接，恢复仍要写代码</h3>
<p>使用 Hibernation WebSocket API 时，DO 在满足闲置条件后可以从内存中移除，客户端连接仍由 Cloudflare 保持。下一条事件到来，对象重新构造，应用从存储和连接附件恢复需要的状态。<a href="https://developers.cloudflare.com/durable-objects/best-practices/websockets/" target="_blank" rel="noopener noreffer ">WebSocket Hibernation</a></p>
<figure><a class="lightgallery" href="/images/game-backend-supabase-cloudflare-do/04-hibernation-storage.svg" title="同一房间进入 Hibernation 后内存被回收，连接附件和已确认写入的存储保留，唤醒时重新构建内存" data-thumbnail="/images/game-backend-supabase-cloudflare-do/04-hibernation-storage.svg" data-sub-html="<h2>图 4：Hibernation 保留连接的能力，与应用恢复房间状态的职责</h2>">
        
    </a><figcaption class="image-caption">图 4：Hibernation 保留连接的能力，与应用恢复房间状态的职责</figcaption>
    </figure>
<p>部署更新或其他重启不保证连接继续存在。客户端仍要检测断线、重新连接，并取得完整快照。只有先恢复到当前状态，才能继续解释后面的增量消息。<a href="https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/" target="_blank" rel="noopener noreffer ">DO 生命周期</a></p>
<h3 id="63-按玩法选择更新频率">6.3. 按玩法选择更新频率</h3>
<p>回合制游戏可以收到操作后才更新状态，等待下一位玩家时进入闲置。实时动作游戏则可能需要固定步长的模拟循环，持续处理移动、碰撞和倒计时。持续运行的定时器会阻止休眠，所以“连接放在 DO”本身不会让活跃对局的计算时间免费。</p>
<p>9 月 29 日补充的房间示例，给出了比较具体的实现：默认以 50 毫秒为步长，只在有移动时运行循环；新连接先得到完整快照，后续收到增量帧；输入带递增序号，服务端返回已经应用的序号；活动停止、成员变化和周期检查点会保存状态。</p>
<p>这些是一个教学模板的行为。50 毫秒不是平台对所有游戏的统一帧率保证，检查点之间的移动也可能在异常重启时丢失。真正的游戏需要根据可接受的数据丢失范围，选择保存时机；聊天记录、回合操作、最终成绩与瞬时位置，通常不应使用完全相同的策略。</p>
<h2 id="7-agent-的部署方式也随之改变">7. Agent 的部署方式也随之改变</h2>
<p>Supabase 阶段，Agent 主要面对资源开通、表结构、数据权限和 SDK 接入。采用 Worker／DO 后，Agent 还要编写真正执行游戏规则的服务端代码，并把它部署成一个持续可访问的后台。</p>
<p>当前 CLI 的基本过程是：创建一个独立 Worker，取得稳定的运行地址；把游戏后端的 JS／TS 打包成 ES module；根据配置声明 DO 类；部署成功后，再把运行地址接进 H5。后续修改同一游戏时，沿用原 Worker 更新代码，已有 DO 命名空间和数据继续保留。</p>
<p>这让模型可以把同一套游戏需求拆成浏览器表现与服务端规则两部分，在相近的语言和工程目录中修改。它也把更多正确性要求带进了代码生成：输入验证、私有状态过滤、重连、存储版本和前后端协议，都需要被示例与检查覆盖。</p>
<p>平台为这类修改补了几层保护。部署携带资源版本，防止两次并行修改相互覆盖；中断的操作保留原请求编号和输入，恢复时核对原操作，减少重复创建资源；已有 DO 绑定不能在普通部署中被随意移除、改名或换类。部署失败时，还要区分“新版本失败”与“旧版本仍在服务”。</p>
<p>这些能力属于我们封装的管理链路。当前部署配置只支持指定范围的模块、同脚本 DO 绑定和普通变量，没有开放完整的 Wrangler 配置和全部 Cloudflare 资源类型。选用 Cloudflare 产品，不意味着平台自动获得了它的所有配置能力。</p>
<p>对于 Agent，另一个实际问题是复制项目。如果新游戏直接继承旧游戏的 Worker 绑定，两个作品就可能修改同一个后台。当前内部 CLI 会结合项目标识检查隐式复用，指引要求新游戏和 Remix 使用独立绑定；对原游戏的继续编辑则使用原资源。这与当初 Supabase 阶段坚持项目隔离，解决的是同一类数据归属问题。</p>
<h2 id="8-两套方案如何比较">8. 两套方案如何比较</h2>
<p>下面比较的是“我们采用的独立托管 Supabase 后台”与“平台管理的 Worker／DO 后台”。如果给 Supabase 再配上独立房间服务，或者改成共享多租户数据库，成本和运行方式都会改变。</p>
<table>
  <thead>
      <tr>
          <th>维度</th>
          <th>Supabase 方案</th>
          <th>Worker／DO 方案</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>最先得到的能力</td>
          <td>关系数据、API、权限与实时消息组件</td>
          <td>自定义入口、按对象执行的逻辑与持久状态</td>
      </tr>
      <tr>
          <td>权威游戏规则</td>
          <td>放在数据库函数、独立服务或客户端等位置，需要明确选择</td>
          <td>可以直接放进房间 DO，规则仍由游戏实现</td>
      </tr>
      <tr>
          <td>数据组织</td>
          <td>表、行、索引和事务，适合关系查询</td>
          <td>对象私有的 SQLite／键值数据，适合围绕一个房间处理</td>
      </tr>
      <tr>
          <td>扩展方式</td>
          <td>调整实例、数据库结构和消息分发方式</td>
          <td>把不同房间分到不同对象；单个热门房间仍有边界</td>
      </tr>
      <tr>
          <td>闲置资源</td>
          <td>受实例休眠窗口与存储配置影响</td>
          <td>可按对象闲置，实际能否休眠取决于代码和连接方式</td>
      </tr>
      <tr>
          <td>全球访问</td>
          <td>取决于实际数据库、Realtime 和代理部署位置</td>
          <td>Worker 入口分布广，具体房间 DO 仍有实际运行位置</td>
      </tr>
      <tr>
          <td>Agent 的工作</td>
          <td>写表结构、权限、数据调用与客户端同步</td>
          <td>写后端规则、协议、存储与部署配置</td>
      </tr>
      <tr>
          <td>运维关注点</td>
          <td>实例状态、建表与迁移、连接与协议、自动启停</td>
          <td>对象身份、热点、休眠、版本兼容、部署和状态恢复</td>
      </tr>
      <tr>
          <td>跨房间统计</td>
          <td>关系库内通常容易表达</td>
          <td>需要额外汇总路径，不能直接跨对象做普通 SQL JOIN</td>
      </tr>
  </tbody>
</table>
<h3 id="81-成本要按生命周期计算">8.1. 成本要按生命周期计算</h3>
<p>对于早期每项目独立实例的方案，可以先按下面的结构理解费用：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">实例方案成本
</span></span><span class="line"><span class="cl">  ≈ 各实例计费运行时长 × 对应计算单价
</span></span><span class="line"><span class="cl">   + 各实例计费存储量 × 存储单价
</span></span><span class="line"><span class="cl">   + 网络、备份及其他服务费用
</span></span></code></pre></div><p>这里影响计算费用的，是实例实际处于计费运行状态的时长；影响存储费用的，还包括当时托管产品的配置下限。项目数量持续增加，即使大部分项目没有玩家，也需要跟踪保留数据带来的费用。</p>
<p>Worker／DO 的成本则需要把平台套餐、入口 Worker、对象请求、对象活跃时间和存储读写分开看：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Worker／DO 方案成本
</span></span><span class="line"><span class="cl">  ≈ 平台与 Worker 费用
</span></span><span class="line"><span class="cl">   + DO 请求及计费活跃时长
</span></span><span class="line"><span class="cl">   + DO 存储读写与保留数据
</span></span><span class="line"><span class="cl">   + 其他使用到的服务费用
</span></span></code></pre></div><p>DO 闲置并满足休眠条件时，可以避免持续的 duration 费用；活跃对局、不能休眠的连接或定时器仍会产生费用，保存的数据也继续计费。Workers for Platforms 本身另有套餐与用量口径，因此不能只套用一个普通 Worker 的起步价格，来估整个平台的账单。<a href="https://developers.cloudflare.com/durable-objects/platform/pricing/" target="_blank" rel="noopener noreffer ">DO 计费</a>、<a href="https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/reference/pricing/" target="_blank" rel="noopener noreffer ">Workers for Platforms 计费</a></p>
<p>9 月的历史记录支持我们当时正在压缩 Supabase 的闲置开销，也支持团队希望选择更轻的运行单元。记录中的节省金额包含测算，没有同口径的迁移前后结算账单可以证明一个确定降幅，所以这里保留成本结构，不把预估节省写成已实现收益。</p>
<h3 id="82-数据库能力仍然有价值">8.2. 数据库能力仍然有价值</h3>
<p>若游戏主要需要长期存档、关系复杂的背包和道具、运营查询、交易记录或跨房间排行榜，PostgreSQL 的查询与事务能力仍然很有价值。Supabase 也仍适合快速构建以数据为中心的应用。</p>
<p>DO 的强项更集中在“这组玩家共享的状态由谁处理”。一个房间内的竞争操作可以集中处理；但各房间都有自己的数据后，全服排行榜就需要另一个汇总设计。可以按玩法将结果发给汇总服务或关系数据库，但这是额外的架构工作，也要定义重复上报和延迟可见的处理方式。</p>
<p>我们后期的选择反映了当时的需求权重：小游戏越来越需要服务端实时裁决，而许多作品没有用到一整套独立关系后台的全部能力。对于同时需要复杂业务数据与实时房间的产品，保留数据库，再配合 DO 或独立房间服务器，也是合理的组合。</p>
<h2 id="9-do-方案的几个边界">9. DO 方案的几个边界</h2>
<h3 id="91-房间的位置仍然影响延迟">9.1. 房间的位置仍然影响延迟</h3>
<p>同一个 DO 会把房间请求汇到一个实际的执行位置。它不会因为玩家分布在多个国家，就为每名玩家同时运行一份可以独立裁决的房间。</p>
<p>Cloudflare 文档说明，首次访问会影响对象放置位置，<code>locationHint</code> 是尽力满足的提示；从不代表真实玩家地域的位置提前创建对象，也可能影响延迟。因此，需要从实际目标地区测量连接建立、首个快照、输入到确认的延迟。国内和海外访问、代理线路、房主设备等因素，都应在比较时固定或单独记录。<a href="https://developers.cloudflare.com/durable-objects/reference/data-location/" target="_blank" rel="noopener noreffer ">DO 数据位置</a></p>
<p>本文能够说明迁移减少了旧路径中的房主往返，但没有把它扩展成“所有地区访问都会更快”的结论。</p>
<h3 id="92-横向增加房间不等于单个房间无限扩容">9.2. 横向增加房间，不等于单个房间无限扩容</h3>
<p>DO 单个对象的执行是单线程的，并受到 CPU、内存、存储和请求处理能力的约束。一个对象承担过多玩家、复杂物理计算或大量消息扇出，仍然可能成为热点。<a href="https://developers.cloudflare.com/durable-objects/platform/limits/" target="_blank" rel="noopener noreffer ">DO 平台限制</a></p>
<p>把一万个独立小房间分到多个对象，与让一万个玩家共享同一个不可拆分的大世界，是不同的问题。后者需要空间分区、兴趣管理和跨分区交互，或者选择专门的游戏服务器。Worker／DO 为小游戏提供了方便的执行单元，但游戏规模和规则仍决定了需要怎样拆分。</p>
<h3 id="93-房间码连接和业务身份各有职责">9.3. 房间码、连接和业务身份各有职责</h3>
<p>房间码用来找到对象，不能单独证明某人有权进入私密房间。WebSocket 已连接，也不能证明它可以修改某个玩家的存档。游戏需要验证访问身份与操作权限，并只向每个玩家发送他可以知道的状态。</p>
<p>例如手牌、战争迷雾或隐身玩家的位置，要在服务端构造消息时过滤。把完整信息发到浏览器后再隐藏显示，无法实现相同的数据访问边界。当前房间示例已经演示按观察者筛选状态，但具体玩法仍需自己的规则。</p>
<h3 id="94-发布代码与迁移数据是两个动作">9.4. 发布代码与迁移数据是两个动作</h3>
<p>更新 Worker 时，客户端可能还在运行旧版 H5。消息结构改变后，需要版本检查、兼容处理或明确的重新加载提示。DO 内的表结构和快照格式也需要演进，回滚代码不一定能自动回滚数据。</p>
<p>Supabase 的表和 RLS 更容易用标准 SQL 工具表达；DO 方案则更多依赖对象身份、WebSocket Hibernation、Alarms 和 Cloudflare 的运行接口。将来迁出时，要同时考虑数据导出、房间寻址和恢复语义。供应商绑定的成本应与当前获得的运行便利一起评估。</p>
<h2 id="10-这次切换实际完成到了哪里">10. 这次切换实际完成到了哪里</h2>
<p>截至 2026 年 10 月 14 日核对的主分支，新在线能力的 Agent 指引已经转向平台管理的 Worker／DO，Supabase 的旧工具与 Skill 从新运行链路中停用。仓库也已经具备 Worker 资源管理、部署恢复、日志查询和联机房间示例。</p>
<p>同时，旧 Supabase 的资源服务、数据访问路径和存量维护逻辑仍然存在。当前指引明确保留已有游戏的数据与接入，迁移需要针对具体游戏执行。这次切换可以确认的是新方案入口与工具链的变化，现有证据没有证明所有历史游戏已经完成迁移，也没有证明所有新生成游戏都通过了真实多人验收。</p>
<p>对具体游戏，验收仍要从玩法出发：两个独立客户端是否看到同一结果；晚加入者是否拿到正确快照；断线恢复后是否继续原有状态；重复操作是否重复加分；服务端拒绝操作时，客户端是否能正确回退；发布后，旧客户端和已有数据是否仍然可用。部署成功只是这些检查的起点。</p>
<p>最初选择 Supabase，使我们能够尽快验证 Agent 能否生成有数据、有房间的游戏。后来选择 Worker／DO，则把房间规则的执行位置、对象的闲置方式和后端代码部署，更紧密地对应到游戏的运行过程。下一次选型仍然要回到具体玩法：数据之间需要什么关系，谁决定共享状态，以及玩家离开后还必须保留什么。</p>
<hr>
<p>本文的历史部分依据 2026 年 4—9 月相关讨论及仓库提交整理，当前实现核对至 2026 年 10 月 14 日。公开产品能力通过正文中的官方文档核对；讨论中的目标、个别测试与成本测算，均按各自范围表述。房间示例和结构图用于解释机制，不代表一次新的线上性能测试。</p>]]></description></item></channel></rss>