关于 cloudflare-backend
cloudflare-backend 是用于在 Cloudflare Workers 上构建和修改 TanStack Start 后端的编码技能,指导架构、依赖组合、入口点和错误处理,并要求遵循项目现有结构。
主要功能
- 01划分领域、应用和基础设施层
- 02让入口点专注于边界职责
- 03在 app-deps.ts 中组合依赖
- 04由应用处理器协调用例
- 05处理传输契约与错误
- 06追踪跨系统边界的调用流程
适用场景
- 01规划新的后端功能
- 02审查入口点或处理器
- 03为 Worker 调用配置依赖
- 04修改服务器函数的错误行为
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ alexander-zuev/ agent-skills --skill cloudflare-backend安装后在 Claude Code 中输入/cloudflare-backend确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/database-workflow.mdentrypoints-and-application.mderror-paths.mdmeasure-worker.mdoperations.mdsecurity.mdtesting.mdtransport-contracts.mdworkers-kv.mdscripts/measure-row.sh详细介绍
后端架构 — 在 Cloudflare Workers 上构建整洁的后端
项目应保持清晰的依赖方向、明确的边界、可测试性和单一组合根,但可以采用 DDD 或六边形架构。识别并遵循现有架构;不要把 DDD 术语强加给端口与适配器代码。标记为 [detect: …] 的章节指出编辑前需要查看的证据。根据契约和职责选择参考代码,而不是根据仓库年代。现有的 RPC 信封属于兼容性场景,不是裸值服务端函数的默认模式。
存在 Effect 时,应将 Context.Tag 服务视为端口,将 Layers 视为适配器构造器和依赖组合机制,将 Effects 视为带类型的程序。保持纯领域逻辑纯粹,在组合根提供 Layers,并且只在入口点/运行时边界运行 Effects。不要创建隐藏的全局运行时或重复记录错误。
后端位于 TanStack Start 应用中:apps/<app>/src/server/。不要单独创建 Worker 应用。
分层
- 领域层 — 实体、值对象、纯业务逻辑。不依赖其他层,且与框架无关。
- 应用层 — 用例和处理器。协调领域模型与出站端口。依赖领域层和应用层自有的端口契约。
- 基础设施层 — 数据库、外部 API、队列、存储和子进程适配器。实现出站端口,但不负责用例流程。
- 入口点 — 接收外部输入或系统边界之外的控制,并将其转换为对应用层的调用。
server/
├── entrypoints/ # functions/ (server fns), queue/, scheduled/, middleware/ — transport only
├── application/ # message bus, handlers, registry
├── domain/ # messages/, models/, services/, errors/ — pure, framework-free
└── infrastructure/ # app-deps.ts, persistence/, auth/, resilience/, external clients
依赖向内流动。领域层不从其他层导入任何内容,包括持久化架构中的 Db* 行类型。
标准,但尚未全面实践:仓库接口属于领域层(旧代码将其保留在
infrastructure/persistence/repositories/interfaces.ts中)。新代码应遵循此标准;不要在无人要求时改动现有文件。
识别系统入口点
入口点是外部输入或控制跨越明确定义的系统边界的位置。
指出边界、外部调用方或运行时,以及接收调用的操作。 根据调用契约推导入口点;通过代码定位它们。
示例:Worker fetch → API 路由/服务端函数、scheduled 和 queue;Durable Object RPC、fetch 和 alarm;Workflow run 和外部事件;本地 CLI 输入。
Webhook 使用 API 路由。 内部 SDK 回调和 Workflow 步骤回调不会构成新的系统入口点。
引导依赖
代码入口点用于初始化或注册系统,例如 server.ts 或类构造函数。
操作入口点接收外部调用。
在拥有其生命周期的作用域内构建依赖:调用、实例或事务。 通过上下文或显式参数传递依赖。
例如,src/server.ts 会为每次 HTTP、队列和定时任务调用执行 createAppDeps(env, ctx)。
infrastructure/app-deps.ts 负责依赖图。
将依赖构造与应用策略分开。 内部服务接收依赖;它们不构造提供方,也不检索运行时绑定。
DI — AppDeps 组合根
将应用装配代码放在 infrastructure/app-deps.ts 中;启动代码负责提供运行时输入。
createAppDeps(env, ctx)用于构建仓储、服务和网关;服务通过once()延迟记忆化。- 启动代码通过请求上下文或项目的组合中间件提供
AppDeps:context.deps.services.messageBus()。 - 处理器签名:
(message, deps: AppDeps) => Promise<TResult>。 - 通过
once()记忆化的服务不得捕获事务范围内的仓储——它们的生命周期超出任何事务。 - 每个条目都是某个进程外部对象的客户端——数据库、队列、Durable Object、Workflow、外部 API。将它们分组:
services(封装在我们接口中的客户端,例如queueClient(): IQueueClient)、clients(原始 SDK 客户端)、dos(Durable Object 客户端)、workflows。接口中绝不能出现原始绑定类型。 - 规则不是依赖,数据也不是依赖。 策略对象(
DestinationPolicy、RateTable、允许列表、由配置驱动的决策)应作为纯函数归属领域层;处理器通过readDb从数据库读取其输入并调用该函数。根据消息类型命名的条目(commands())始终是错误的——应按能力命名。
应用返回一个原始值,失败则抛出异常。
// The shared function middleware owns error conversion.
export const createProduct = createServerFn({ method: 'POST' })
.middleware([depsMiddleware, requireAuth])
.validator(createProductSchema)
.handler(async ({ data, context }) => {
const result = await context.deps.services.messageBus().handle(
createCommand('CreateProduct', { ...data, userId: context.user.id }),
)
return result
})
错误处理
此技能负责传输契约、错误转换、日志记录责任归属和外部重试。 类型、架构和通用模块设计请参阅 typescript-standards。
编辑服务器函数或存根前:
- 阅读它的调用方、共享契约和一个类似的现有函数。
- 阅读
src/start.ts以及已注册的错误、身份验证和 CSRF 中间件。 - 阅读错误载荷模块以及该中间件使用的任何已安装辅助工具。
- 确认成功类型、失败通道和日志记录责任方。
- 说明规范、参考代码和当前项目之间的实质差异。
服务器函数、HTTP、Durable Object RPC 或 WebSocket 相关内容,请阅读传输契约。 更改失败行为或外部客户端时,请阅读错误路径和重试。
当前服务器函数返回原始值。共享中间件会将抛出的失败转换为简单的带标签载荷,且只转换一次。 复用全局中间件注册。不要在每个函数中单独安装第二层错误边界。 通过现有中间件保留 Router 重定向和未找到时的控制流。
对于存根,请使用项目的未实现辅助工具,并声明预期的返回类型。 保留必需的验证和身份验证。不要伪造成功,也不要在请求范围之外添加生产行为。
入口点与应用编排
对于编写或审查入口点、应用处理器、用例或服务适配器的任何任务,在提出或编辑代码前,请阅读 references/entrypoints-and-application.md。
可以按任何实用顺序检查各层。在接受设计前,应追踪入口点、应用层、领域层和基础设施层之间的完整流程。
应用流程应一目了然。典型的处理器会加载数据、准备或决策、调用某项能力、持久化并返回。这是一种结构,并非规定的操作数量。单步读取是有效的。深层嵌套、重复分支,或将所有工作都转交给一个宽泛服务的处理器,都表明职责归属不清。
入口点规则
操作性入口点只负责边界工作。分配这一职责前,先执行上述边界测试。
- 消息总线架构: 在需要时进行验证和授权,构建或解析
Command | Query | Event,等待bus.handle(message),然后转换结果。 - 直接处理器架构: 在需要时进行验证和授权,构建类型化输入,等待一个应用处理器,然后转换结果。
- 每个平台边界负责自己的交付语义:HTTP 响应、队列确认或定时调用失败。工作流编排负责持久化步骤配置。
应用规则
应用处理器是主要的编排者。它负责用例策略、计划、顺序、端口协调、事务边界、清理或补偿,以及有用的错误处理行为。
它不负责传输转换、框架代码、提供方命令、SQL、SDK 调用或领域不变量。
使用转发包装器测试:如果移除处理器后,仍会调用同一个宽泛服务并返回相同结果,就检查该服务。如果它协调多个外部能力,就将这些能力拆分为端口,并将执行顺序移入处理器。对于真正只涉及一个端口的读取,不要凭空添加步骤。
业务逻辑放置位置
| 层 | 业务逻辑? |
|---|---|
| 入口点 | ❌ 解析、授权、分发、响应 |
| 处理器 | 仅编排策略 |
| 领域模型 | ✅ 不变量、状态转换 |
| 领域服务 | ✅ 跨实体算法 |
| 仓储 | ❌ 仅负责持久化 |
例外(“双重门槛”): 当操作属于传输协调时,入口点可以直接调用专用服务客户端。该客户端必须只暴露一项范围明确的能力。绝不允许在入口点中内联用例逻辑。
服务端心智模型 — 每个入口点的职责都相同
Message bus: Entrypoint → Command | Query | Event → Bus → Handler → Domain + Ports
Direct handler: Entrypoint → Typed input → Handler → Domain + Ports
已识别的操作性入口点会向应用分发请求,并将结果转换为符合其外部契约的形式。传输方式并非关键。
规则:
- 入口点应保持精简:在需要时验证 → 在需要时授权 → 分发 → 转换。
- 消息是纯数据,不包含方法。
- 处理器负责编排。处理器中不包含业务逻辑。
- 领域层保持纯粹。不导入框架或基础设施。
- 依赖向内流动,绝不反向流动。
- 仓储模式:接口定义在领域层,实现位于基础设施层。
消息 — Command / Event / Query
| 类型 | 处理器 | 幂等性 | 语义 |
|---|---|---|---|
| Command | 恰好 1 个 | 在 UoW 中声明 id | 修改 |
| Event | 0..N 个订阅者 | 每个订阅者单独声明 | 领域事实,扇出 |
| Query | 恰好 1 个 | 无 — 不含 id | 读取,绕过 UoW |
消息的基本事实:
type: 'command' | 'event' | 'query'判别字段 +name字面量 + 可选的userId(可观测性)。id为uuid | externalMessageId。外部 ID 是命名空间化的提供方键 —stripe:event:evt_123— 可为 Webhook 实现基于提供方键的幂等性。工厂函数(createCommand(...))默认生成 UUID。[detect base.messages.ts]:当前生成版本使用z.uuidv7()(按时间排序);旧版本使用z.uuidv4()。[detect]消息位置:与客户端共享 →packages/core/src/domain/**;仅服务端使用 →apps/<app>/src/server/domain/messages/。- 载荷 ID 使用品牌化 Schema(
UserIdSchema、ProductIdSchema);Zod v4 辅助函数(z.uuid()、z.uuidv7()、z.iso.datetime())。
export const createProductSchema = baseCommandSchema.extend({
name: z.literal('CreateProduct'),
productName: z.string(),
userId: UserIdSchema,
})
注册表
使用一个注册表文件(application/registry/registry.ts 或 application/handlers/registry.ts)将名称关联到处理器;satisfies CommandRegistry / QueryRegistry 可让编译器验证是否完整。如果添加消息时编译器没有报错,那么该消息就没有接入注册表。
export const COMMAND_HANDLERS = {
CreateProduct: createProduct,
} satisfies CommandRegistry
export const EVENT_HANDLERS: EventRegistry = {
ProductCreated: [
{ id: 'project-product-view', mode: 'state', handle: projectProductView },
{ id: 'track-product-created', mode: 'effect', handle: trackProductCreated },
],
}
状态订阅者与副作用订阅者——关键的分发规则
判断每个事件订阅者类型的标准:它会写入我们的数据库吗?
| 类型 | 运行于 | 幂等性 | 用途 |
|---|---|---|---|
state | 自己的、按订阅者划分的 UoW | 以事件 ID(及订阅者 ID)为键的回执 | 数据库写入:投影、反规范化 |
effect | 无事务、无认领 | 幂等键交由目标系统处理 | 分析、电子邮件、外部 API |
失败的 state 订阅者只会回滚自己的 UoW。effect 订阅者必须能够安全地重新运行——绝不要用数据库认领来包装副作用(先认领再执行副作用,崩溃时会丢失副作用;先执行副作用再认领,则会留下孤儿窗口)。同时执行这两类操作的订阅者 → 拆分。
[detect registry.ts] 接线:较新一代会注册 { id, mode, handle } 形式的订阅者对象,分发机制会为每个 state 订阅者开启 UoW;较早一代会注册普通的 (event, deps) 函数,每个 state 订阅者都会开启自己的 UoW,并以事件 ID 为键。
消息总线
从类型化消息到处理程序的单一关口;总线负责路由,不负责开启事务。事件订阅者会并行运行(Promise.allSettled)——一个失败不会中止其他订阅者。重复消息处理:事件订阅者抛出的 DuplicateMessageError → 视为成功并吞掉;命令抛出的则会重新抛给调用方;队列消费者会将重复消息确认并跳过。
工作单元
命令处理程序开启 UoW(而非总线):deps.uow.run(messageId, (repos, emit) => …)——事务范围内的仓储,以及用于发出领域事件的 emit。事件 state 订阅者各自拥有独立的 UoW(根据上文所述的对应一代接线方式开启)。一个原子范围按顺序执行:
- 认领回执——将
message.id插入message_receipts;若冲突 →DuplicateMessageError。 - 处理程序写入。
- 将发出的事件持久化到
message_outbox——状态变更与事件一同提交或回滚。 - 提交。
[detect unit-of-work implementation]:
- D1:不支持交互式事务——将回执、写入和 outbox 暂存到一个
db.batch()中,通过重试提交;回执 UNIQUE 冲突 →DuplicateMessageError。 - Postgres:使用真正的事务;抛出的错误会回滚事务,采用带品牌标记的
Tx类型,并通过 AsyncLocalStorage 收集事件。
事务规则:
- 事务内不得进行跨边界 I/O——持有连接时不得执行 fetch/队列/R2 操作(Hyperdrive 连接池耗尽)。
- 每个事务仅处理一个聚合。在调用方边界重试整个 UoW。绝不要嵌套 UoW。
- Postgres:每次调用使用一个连接。 并行的工作单元会等待彼此的事务。在工作单元内,事务外的读取(
readDb)会等待其自身连接;适配器会直接抛错,而不会一直挂起。 - 查询完全绕过 UoW;读取路径可以跳过仓储——查询处理程序可以内联 Drizzle
select({...})→ DTO。DTO 是产品资源,不是数据行。不要泄漏存储层的偶然细节。除非调用方请求,否则不要加载开销大的关联数据。
Outbox 中继
message_outbox 中的事件由中继投递,而非由请求投递:通过定期扫描(cron */1 * * * *,或由中继 DO 闹钟排空并投递到 Queues——[detect wrangler.jsonc])。提交后调用 deps.hooks.onEventsCommitted(),通过 waitUntil 唤醒中继,这只是延迟优化——定期扫描才是事实来源;唤醒信号丢失不会导致事件丢失。
幂等性
所有会产生变更的操作都必须具备幂等性。分为三个层次:
- 消息回执——由 UoW 自动处理(见上文)。可以安全地重试命令/重新投递事件。
- 外部副作用——传入根据稳定操作属性派生的幂等键:
由 Webhook 驱动的消息会将提供方的键用作消息 ID(await stripe.charges.create({ amount }, { idempotencyKey: `${runId}-${nodeId}` })stripe:event:evt_123)。 - 数据库层面——使用原子插入,而非先检查再插入:
.onConflictDoNothing()/.onConflictDoUpdate()。不存在 TOCTOU 窗口。
仓储
写入和实体读取都通过仓储完成。
- 封闭的方法列表:
byId/findByX/insert/save/delete。不提供通用的update(id, fields)。 insert≠save:对已存在的数据执行insert会失败(AlreadyExists);save会进行版本保护(版本过旧时抛出RevisionConflictError)。- 编写处理程序前,先为每个聚合选择插入类型。绝不要先检查再插入:两个并发请求可能都会通过检查。
- **仅创建一次:**重复创建属于错误。
insert会失败并返回AlreadyExists,调用方负责报告错误。 - **自然键、可重复到达:**相同输入必须返回相同聚合,例如通过哈希标识的文件。
insert执行ON CONFLICT (key) DO NOTHING RETURNING id,然后读取现有 ID。它会返回 ID,且仅为新行派发事件。处理程序返回 ID,不报告“新建”或“已存在”。
- **仅创建一次:**重复创建属于错误。
- 每个仓储都为持久化实体定义一个
toDomain(row: DbRow): Entity映射器。 toDomain构建实体的普通数据,并将其传入Entity.restore(data)。toDomain将字段名称和平铺的数据库行映射为嵌套的普通数据。它不验证或解析值。- 当数据库中的平铺字段可空性与领域联合类型冲突时,在调用
restore前使用范围尽可能窄的类型断言;构造函数负责强制执行领域不变量。 - 在仓储内部,将
entity.toPlainObject()映射为数据库插入或更新所需的结构。 - 迁移旧数据库值。迁移后不要在
toDomain中保留旧值转换逻辑。 - **使用 Drizzle,处理程序中绝不使用原始 SQL。**仅当 Drizzle 无法表达查询时才使用原始 SQL,并在仓库中记录说明。
export class ProductRepository implements IProductRepository {
async byId(id: ProductId, userId: UserId): Promise<Product | null> {
const row = await this.db.query.product.findFirst({
where: and(eq(product.id, id), eq(product.userId, userId)),
})
return row === undefined ? null : this.toDomain(row)
}
private toDomain(row: DbProduct): Product {
return Product.restore({
id: row.id,
name: row.name,
state: { status: row.status },
})
}
}
Drizzle 表定义使用返回索引的数组:
sqliteTable('t', { ... }, (t) => [index('idx').on(t.col)])
领域实体
仅在实体具有标识、状态转换和不变量时使用实体模式;其他情况使用普通类型。
- 基类
Entity:addEvent/collectEvents/clearEvents;通过toPlainObject()序列化。 - 持久化实体公开
toPlainObject(): EntityData和static restore(data: EntityData): Entity。 restore接受与toPlainObject返回值完全相同的类型。- 使用私有构造函数。在构造函数中或其调用的一个共享断言中验证关键业务不变量。
- 不要在领域模型中重复 HTTP 或 I/O 边界上的 Zod 验证。
- 不要在领域模型中重复数据库类型、可空性或约束检查。
- 领域断言用于补充边界验证,处理类型和数据库架构无法表达的规则。
create、restore和状态转换必须使用相同的不变量验证。restore重建领域状态,但不发出领域事件。- 领域实体不得导入数据库类型,也不得公开
fromPersistence和toPersistence方法。 static create(...)遇到不变量违反时会抛出领域错误。
export class Product extends Entity {
private constructor(private readonly data: ProductData) {
super()
assertValidProduct(data)
}
static restore(data: ProductData): Product {
return new Product(data)
}
toPlainObject(): ProductData {
return { ...this.data }
}
}
每次状态转换都应是一个实体方法:
- 该方法检查规则并应用变更。处理程序绝不先调用
is…检查,再单独调用record…方法。 - 该方法返回带标签的结果联合类型。重复操作返回“已完成”值。状态错误时返回拒绝结果。绝不返回
void并静默执行return。 - 对实体自身状态的判断应作为结果返回,而不是作为处理程序分支。处理程序根据结果决定保存、记录日志或回复。
- 纯查询(
is…Due、to…Request)不会修改任何内容。仅当处理程序需要在不修改状态的情况下调用时,才将其设为公开方法。
export type MarkLiveOutcome =
| { readonly kind: 'live' }
| { readonly kind: 'alreadyLive' }
| { readonly kind: 'archived' }
markLive(at: Date): MarkLiveOutcome {
if (this.status === 'live') return { kind: 'alreadyLive' }
if (this.status === 'archived') return { kind: 'archived' }
this.status = 'live'
this.updatedAt = at
return { kind: 'live' }
}
仅当状态发生变化时,处理程序才保存:
const outcome = playbook.markLive(now)
if (outcome.kind === 'archived') return { kind: 'archived' }
if (outcome.kind === 'live') repository.save(playbook)
/core 包边界
packages/core 有两个入口点——index.ts(完整,服务端)和 client.ts(浏览器安全)。浏览器代码只能导入 @<scope>/core/client;此子路径是防火墙,可避免将仅限服务端的模块打包进去。
Core 包含浏览器安全的契约和领域类型,以及项目明确使用的、彼此隔离的服务端基础设施。仅当现有信封契约需要时,才需要线缆信封模块。数据库架构和迁移位于应用中(apps/<app>/src/server/infrastructure/{db|persistence}/),而不是 core 中。
已发布契约
更改结构前,先对接口进行分类。
| 接口 | 示例 | 破坏性变更规则 |
|---|---|---|
| 内部 | 随 Web 客户端一起发布的 TanStack 服务端函数 | 可在同一次部署中协调破坏性变更 |
| 已发布 | MCP 工具输入/输出、扩展契约、HTTP /api、出站 Webhook 负载、其他客户端导入的 @<scope>/core/client 架构 | 先并存、发出警告,然后移除 |
已发布结构可以更改,但不能在替换它的同一次变更中消失。
- 发布新字段、工具或结构,同时保留旧项。
- 在机器可见的位置将旧项标记为弃用:HTTP 使用
Deprecation+Sunset,MCP 工具或参数使用deprecated: true,导出符号使用@deprecated。 - 说明何时移除(日期、版本,或连续 N 天无流量)。
- 窗口期过后,在单独的变更中删除旧项。
保留一个业务核心。使用同一个处理程序填充两种结构。不要为 /v1/ 创建分叉处理程序。不要永远保留双字段。
禁止静默重命名。只有在无法联系到的客户端仍依赖旧路径时,才保留该路径。
已发布接口规则:
- 消费方必须忽略未知字段。
- DTO 应隐藏存储方面的意外细节。不要发布持久化结构。
- 默认不包含开销较大的关联数据。由调用方选择启用。
- 任何向代码开放的操作(MCP、HTTP、扩展)都需要速率限制。开销较大的操作应采用更严格的限制。429 响应中返回
Retry-After。保留按客户启用的紧急开关。
分页
- 范围限定在用户内的小型列表:
?page=1&limit=20(最大值为 100)→{ items, page, limit, total, hasNextPage, hasPrevPage }。 - 无界集合(作业历史记录、审计记录、MCP 列表工具):使用游标 +
next令牌。查询方式为WHERE id > cursor ORDER BY id LIMIT n。这些列表绝不使用偏移量。
平台模式
队列消费者
entrypoints/queue/ 中的基于类的消费者:
- 每条消息使用独立的 Sentry 隔离作用域。
classifyForRetry——由类型化错误决定重试还是暂存;采用明确的指数退避并设置上限。- 在 wrangler.jsonc 中配置
max_retries+ DLQ;将有毒消息暂存;重复消息确认后跳过。
Cron / 定时任务
- Cron 注册表(
entrypoints/scheduled/cron-registry.ts)将表达式映射到处理程序;包含 outbox 中继清理任务。 - Cron 不会重试。 每个工作单元都要自行捕获错误;单个失败项不能导致整个清理任务中止。
- 本地触发:
curl "http://localhost:8787/cdn-cgi/handler/scheduled"。
Cloudflare Workflows
- 每个步骤处理一个工作单元——步骤是重试边界。
- 在每个
step.do()内构建依赖项,绝不能在构造函数中构建:Hyperdrive 连接不能跨步骤传递,且引擎可能在步骤之间休眠。 - 先获取再记录:在一个步骤中执行外部调用,在下一步骤中记录结果;重试的步骤绝不能重复执行已提交的操作。
- 意图 → 效果 → 结果:在执行外部操作前,先创建持久化意图(DB 行)。
- 将不可重试的类型化错误映射为
NonRetryableError;内部与外部步骤配置分别携带重试策略。
Durable Objects
用途包括:速率限制(RateLimiterDO)、发件箱中继、Agent/网关(Agents SDK)、容器监管器。使用 Sentry.instrumentDurableObjectWithSentry(...) 进行封装。
每个对象在内存中的构造函数只运行一次,可处理许多请求和定时任务。应在每个方法或定时任务中构建依赖项,而不是在构造函数中构建,并在结束时关闭数据库:Hyperdrive 只会在 Durable Object 进入休眠或被驱逐时清理其套接字,而打开的套接字会阻止其休眠。
迁移:每项变更都要新建迁移,使用唯一标签,并且只追加——绝不编辑或移除已发布的条目。新 DO 默认使用 new_sqlite_classes;仅在有记录在案的理由时才使用 new_classes(存在旧版 DO——不要擅自迁移)。重命名后在代码中使用新的类名;绝不要手动创建用于重命名/转移的目标类。
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["SessionDO"] },
{ "tag": "v2", "renamed_classes": [{"from": "OldDO", "to": "NewDO"}] }
]
Containers
参考用法:在独立的应用目录中放置一个 Python 服务(uv/pyproject);Start 应用的 wrangler containers 条目指向其 Dockerfile,并配有一个经 Sentry 检测的监管 DO(class_name)。仅支持 Linux/amd64;镜像会上传到 Cloudflare Registry。
Workers KV
添加、审查或调整 KV 键之前,请阅读 workers KV:对不会更改的键使用较长的 cacheTtl、使用带版本的键、共享键、每个请求只读取一次,并在发布时写入。
Remote bindings
绑定上的 "remote": true 会将本地开发连接到已部署的资源(会产生费用)。支持:AI、D1、KV、R2、Queues、Vectorize、Workflows。仅限本地:Durable Objects、Hyperdrive、Rate Limiting。Vitest 始终使用本地模拟。
Typegen
配置或绑定变更后,运行项目的 cf-typegen 脚本。绝不要编辑生成的类型。
普通的 wrangler types 会将已配置的环境合并为严格联合类型,并生成 Cloudflare.<EnvName>Env。
只有在有意为单个环境生成类型时,才使用 --env。绝不要用 --strict-vars=false 隐藏差异。
对于跨 Worker RPC,请通过重复使用 -c 标志传入调用方和每个已绑定 Worker 的配置。
在每个环境中保留必需的绑定名称,以便 Wrangler 无需类型断言即可解析 Service<typeof Entrypoint>。
Local Explorer API
Cloudflare Vite 会在 http://localhost:<project-port>/cdn-cgi/explorer/api 暴露本地资源。
从配置或进程输出中确定当前项目 URL。绝不要假设端口是共用的。
使用前,请从该 API 根路径获取 OpenAPI 架构。只能使用有文档说明的操作。
如需进行相关平台变更,请阅读对应范围内的 cloudflare、durable-objects、wrangler 或 workers-best-practices 技能。
Deployment
使用 Workers,绝不使用 Pages。 TanStack Start 应用使用 @cloudflare/vite-plugin:CLOUDFLARE_ENV=<env> vite build 会从 wrangler.jsonc 解析环境,并生成包含计算所得 main/assets 的扁平化 dist/server/wrangler.json——这些字段不是手动编写的。
环境名称和部署脚本因项目而异——请阅读 wrangler.jsonc 和 package.json(例如 dev/preview/prod、dev/staging/test/prod;pnpm deploy 部署到预生产环境,pnpm deploy:prod 部署到生产环境,两者都会先运行迁移)。
Database Workflow
进行架构变更、迁移或数据库查询之前,请阅读 database workflow。其中涵盖 D1 和 Postgres 检测、迁移命令、迁移规则及约束归属。
Testing
编写、审查或加速 Worker、绑定或数据库测试之前,请阅读 Workers testing。
安全与运维
设置 Turnstile、修复问题或迁移 CAPTCHA 时,请先使用官方 Turnstile Spin 技能。 请从其 Agent 工作流开始,而不是手动设置或使用控制面板。
对于身份验证、授权、公开输入、上传或密钥处理,请阅读 security。 对于后端性能、日志记录、指标或告警,请阅读 operations。 若要衡量已部署 Worker 的速度或故障,请阅读 measure a Worker。
密钥 — dotenvx
- 加密的
.env文件会提交到版本库;私钥(.env.keys)会被 gitignore。脚本在dotenvx run --下运行。 - wrangler
secrets.required清单声明生产环境所需的内容。 - 这是这些项目的密钥处理机制,取代
.dev.vars。
尚未采用的标准(应用于新代码;不要大规模改动现有代码)
- 列表分页:用于新的列表端点。遵循已发布的契约规则:用户范围内的小型列表使用 offset 封套;无界集合使用 cursor +
next。 - 在领域层定义仓库接口。
- 每个消息负载都使用带类型标识的 ID(旧版负载使用普通字符串)。
MCP 服务配置
MCP 工具的输入和输出架构属于已发布的契约。更改时应采用重叠方案,先发出警告,再移除旧内容。
claude mcp add --transport http <name> <url> --header "Authorization: Bearer <token>"
Windows 上的 npx 服务需要使用 cmd /c 包装:
{ "command": "cmd", "args": ["/c", "npx", "-y", "@package/name"] }