设计原则
单一 Worker 作为运行时容器。 API、SSR、Cron 和队列都运行在同一个 Worker 里。开发和部署走相同的路径,无需拆分服务,也无需额外的胶水层。
约定优于配置。 不手写 wrangler.jsonc。设置环境变量,scripts/prepare-cloudflare.mjs 自动生成配置、创建资源并应用迁移。
优先使用 Cloudflare 技术栈。 Workers、D1、R2、KV、Queues 和 Cron 是一条集成路径。免费套餐可以跑通整个循环。
自动化优先。 任何环境下都不手动创建资源。prepare-cloudflare.mjs 负责本地和远程的资源供给。
系统概览
几点关键说明:
- 只有一个 Worker 部署。Web 页面、API、Webhook、Cron 和队列消费者都在同一个代码库里,一起发布。不需要运行独立服务。
- 边缘是 Cloudflare 的全球网络。DNS 和 TLS 在那里终结,请求自动路由到最近的 Worker 实例。
- 客户端是 Web 应用(浏览器)和 Chrome 扩展。它们共享
src/frontend/lib/中的大部分前端代码。 - 外部 SaaS 是第三方提供商。应用启动后,在 Admin / Configuration 中按业务域启用和配置。
请求流程
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 DB(META_DB):全局控制状态。整个产品共用一个数据库。存储分片注册表、用户到分片的映射、动态系统配置、OAuth API Access、认证、支付、AI Provider、订阅、Webhook 和通知。通过 ctx.get('metaDb') 访问。
租户分片 DB:用户级别的运行时数据。按地区分片到多个 D1 数据库中。存储积分余额、积分交易、反馈、通知已读状态、AI 异步任务表等。通过 ctx.get('tenantDb') 访问。
为什么要拆分:Meta DB 是所有跨用户数据(支付、分片分配)的单一事实来源。租户分片按地区水平扩展,用户越多只需要增加分片。用户数据在其生命周期内始终存放在同一个分片里。
支持的分片地区:wnam、enam、weur、eeur、apac、oc。通过 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_products 和 ai_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 或调整产品行为不需要。