logo

架构

系统形态、设计决策与数据模型

设计原则

单一 Worker 作为运行时容器。 API、SSR、Cron 和队列都运行在同一个 Worker 里。开发和部署走相同的路径,无需拆分服务,也无需额外的胶水层。

约定优于配置。 不手写 wrangler.jsonc。设置环境变量,scripts/prepare-cloudflare.mjs 自动生成配置、创建资源并应用迁移。

优先使用 Cloudflare 技术栈。 Workers、D1、R2、KV、Queues 和 Cron 是一条集成路径。免费套餐可以跑通整个循环。

自动化优先。 任何环境下都不手动创建资源。prepare-cloudflare.mjs 负责本地和远程的资源供给。

系统概览

flowchart TB subgraph Clients Browser["Browser web app"] Extension["Chrome extension"] end subgraph Edge["Cloudflare edge"] DNS["DNS + TLS"] Routes["Worker routes"] end subgraph Worker["Single Worker deployment"] Entry["Entry src/index.ts"] Web["SvelteKit SSR\nstatic pages"] Api["Hono API\nauth + business"] Webhooks["Payment webhooks"] Jobs["Cron jobs"] Consumers["Queue consumers"] end subgraph Data["Data plane"] Meta["Meta DB\nshard registry\nuser_shards\nauth\npayments\nnotifications"] Shards["Tenant Shard DBs\ncredit balances\nfeedbacks\nAI tasks\n..."] R2["R2\npublic/private objects"] KV["KV"] Queues["Queues"] end subgraph External["External SaaS"] OAuth["Google / GitHub / LinuxDO"] Payment["Dodo / Creem"] AI["OpenAI / Gemini / ..."] Email["Resend / Cloudflare Email"] end Browser --> DNS Extension --> DNS DNS --> Routes Routes --> Entry Entry --> Web Entry --> Api Entry --> Webhooks Entry --> Jobs Entry --> Consumers Api --> Meta Api --> Shards Api --> R2 Api --> KV Api --> Queues Api --> OAuth Api --> Payment Api --> AI Api --> Email Webhooks --> Meta Jobs --> Meta Jobs --> Shards Consumers --> Shards Consumers --> R2 Consumers --> AI

几点关键说明:

  • 只有一个 Worker 部署。Web 页面、API、Webhook、Cron 和队列消费者都在同一个代码库里,一起发布。不需要运行独立服务。
  • 边缘是 Cloudflare 的全球网络。DNS 和 TLS 在那里终结,请求自动路由到最近的 Worker 实例。
  • 客户端是 Web 应用(浏览器)和 Chrome 扩展。它们共享 src/frontend/lib/ 中的大部分前端代码。
  • 外部 SaaS 是第三方提供商。应用启动后,在系统设置中配置单例凭据,在对应提供商工作区管理集合实体。

请求流程

HTTP Request
  ├── /api/* -> Hono API (src/backend/api/)
  └── other  -> SvelteKit SSR (src/frontend/web/)

Cron Trigger    -> src/backend/jobs/index.ts
Queue Consumer  -> src/backend/consumers/index.ts

所有请求都从 src/index.ts 进入。对于 HTTP 请求,它检查路径:/api/* 走 Hono,其余走 SvelteKit。Cron 和队列是独立的入口点,但同属一个 Worker。

API 层

API 在 src/backend/api/index.ts 中被拆分为四个路由组:

认证级别 用途
publicApi 健康检查、认证登录、支付 Webhook、R2 公共读取
authOnlyApi 浏览器 Session 或带 scope 的 OAuth Token 只需要身份的路由
userApi Session 或 OAuth Token + 内测门控 已认证用户 JSON API
adminApi Session 或 OAuth Token + D1 管理员角色 管理员 JSON API

受保护的 JSON 路由必须在路由注册旁直接声明一个 scope。浏览器 Session 直接满足该 scope,OAuth Access Token 必须显式包含它。管理员路由还会校验授权用户当前仍持有 D1 管理员角色。流式传输和对象读写等仅限浏览器的路由会显式拒绝 OAuth Token。

数据架构

两层数据库,各自负责不同的所有权:

Meta DBMETA_DB):全局控制状态。整个产品共用一个数据库。存储分片注册表、用户到分片的映射、动态系统配置、OAuth API Access、认证、支付、AI Provider、订阅、Webhook 和通知。通过 ctx.get('metaDb') 访问。

租户分片 DB:用户级别的运行时数据。按地区分片到多个 D1 数据库中。存储积分余额、积分交易、反馈、通知已读状态、AI 异步任务表等。通过 ctx.get('tenantDb') 访问。

为什么要拆分:Meta DB 是所有跨用户数据(支付、分片分配)的单一事实来源。租户分片按地区水平扩展,用户越多只需要增加分片。用户数据在其生命周期内始终存放在同一个分片里。

支持的分片地区:wnamenamweureeurapacoc。通过 D1_SHARDS 环境变量以 region:count 键值对配置。

新用户分配优先选择 Worker 所在大陆对应的分片,否则选择任意活跃分片:

AS -> apac | EU -> weur | OC -> oc | default -> apac

已有用户始终遵循其 user_shards 记录,即使迁移到其他地区也不会改变。这防止了数据碎片化。

读一致性

每个 D1 数据库有一个主节点。不开启读副本时,所有读写都打到主节点,在高负载下会限制延迟和吞吐量。

生产环境中,prepare-cloudflare.mjs 会自动启用读副本。此后,读取走全球副本节点(离用户更近),写入仍然走主节点。

这带来了一致性问题:用户写入后,后续读取可能命中尚未同步的副本。OPCStack 用 bookmark 来解决这个问题。每个 D1 会话返回一个表示某个一致时间点快照的 bookmark,它通过响应头和 Cookie 流转回客户端,客户端在下次请求时带上它。这实现了单调读("读到自己写入的内容"),而无需分布式事务。

Meta DB 和租户分片 DB 各自维护独立的 bookmark 流,因为它们是拥有各自主节点的独立数据库。

动态配置基础

system_settings 是动态产品配置的单例权威来源。每个业务域有独立版本,管理员 API 可以拒绝过期写入而不耦合其他配置域。payment_productsai_providers 是独立的版本化集合。

敏感值以 AES-GCM 密文和 IV 保存。prepare-cloudflare 首次生成 CONFIG_ENCRYPTION_KEY,并保存在本地生成的 secret 状态或 Cloudflare Worker Secrets 中;它不会写入 D1,也不会在 D1 初始化后被替换。所有运行时业务域都只从 D1 读取配置,不存在业务 ENV 回退。

prepare-cloudflare 自动化

scripts/prepare-cloudflare.mjs 是资源供给的唯一入口:

pnpm dev / pnpm deploy:cloudflare
  -> load env
  -> generate wrangler.jsonc
  -> create D1, R2, KV, Queues, Turnstile (remote only)
  -> generate and apply migrations
  -> wrangler dev / wrangler deploy

本地模式使用占位 UUID 并在本地应用迁移。远程模式创建真实的 Cloudflare 资源,启用读副本,并远程应用迁移。

这是一个架构决策,而不只是便利性考量:固定部署拓扑由环境变量生成,运行时业务设置保存在 D1。添加 Queue、Shard 或 Durable Object 才需要修改固定拓扑;启用 Provider 或调整产品行为不需要。