cloudflare-backend

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Backend Architecture — Clean Backends on Cloudflare Workers

后端架构——基于 Cloudflare Workers 的整洁后端

Projects preserve clean dependency direction, explicit boundaries, testability, and one composition root, but may use DDD or hexagonal architecture. Detect and follow the existing architecture; do not force DDD terminology onto ports-and-adapters code. Sections marked
[detect: …]
name evidence to read before editing. Select reference code by its contract and responsibility, not repository age. Existing RPC envelopes are compatibility cases, not the default for a bare-value server function.
When Effect is present, treat
Context.Tag
services as ports, Layers as adapter constructors and dependency composition, and Effects as typed programs. Keep pure domain logic pure, provide Layers at the composition root, and run Effects only at entrypoint/runtime boundaries. Do not create hidden global runtimes or duplicate error logging.
Backends live inside the TanStack Start app:
apps/<app>/src/server/
. No separate worker app.
项目应保持清晰的依赖方向、明确的边界、可测试性以及唯一的组合根,但可以采用 DDD 或六边形架构。先检测并遵循现有架构;不要把 DDD 术语强行套用到端口与适配器代码上。标记为
[detect: …]
的章节列出了编辑前必须读取的证据。应根据契约和职责选择参考代码,而不是依据代码在仓库中的新旧程度。现有 RPC envelope 属于兼容性场景,并不是裸值 server function 的默认方案。
存在 Effect 时,应将
Context.Tag
services 视为 ports,将 Layers 视为 adapter constructors 和依赖组合,并将 Effects 视为类型化程序。保持纯 domain logic 的纯粹性,在 composition root 提供 Layers,并且仅在 entrypoint/runtime 边界运行 Effects。不要创建隐藏的全局 runtime,也不要重复记录错误日志。
后端位于 TanStack Start 应用内部:
apps/<app>/src/server/
。不创建单独的 worker 应用。

Layering

分层

  • Domain — entities, value objects, pure business logic. Zero dependencies on other layers, framework-agnostic.
  • Application — use cases and handlers. Orchestrates domain models and outbound ports. Depends on Domain and application-owned port contracts.
  • Infrastructure — DB, external APIs, queues, storage, and subprocess adapters. Implements outbound ports without owning use-case sequences.
  • Entrypoints — receive external input or control across the system boundary and translate it into application calls.
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
Dependencies flow inward. Domain imports nothing from other layers — including
Db*
row types from persistence schemas.
Standard, not fully practiced: repository interfaces belong in domain (older code keeps them in
infrastructure/persistence/repositories/interfaces.ts
). Apply to new code; don't churn existing files unprompted.
  • Domain——entities、value objects 和纯 business logic。不依赖其他任何层,且与框架无关。
  • Application——use cases 和 handlers。编排 domain models 与 outbound ports。依赖 Domain 和 application 所拥有的 port contracts。
  • Infrastructure——DB、external APIs、queues、storage 和 subprocess adapters。实现 outbound ports,但不拥有 use-case 序列。
  • Entrypoints——接收外部输入或控制,并将其转换为 application calls。
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
依赖只能向内流动。Domain 不导入其他任何层,包括来自 persistence schemas 的
Db*
row types。
这是标准做法,但尚未在所有项目中落实:repository interfaces 应放在 domain(旧代码将其保留在
infrastructure/persistence/repositories/interfaces.ts
)。新代码应遵循此规则;不要在未要求时改动现有文件。

Identify System Entrypoints

识别系统入口点

An entrypoint is where external input or control crosses a defined system boundary.
Name the boundary, the external caller or runtime, and the receiving operation. Derive entrypoints from that invocation contract; use code to locate them.
Examples: Worker
fetch
→ API routes/server functions,
scheduled
, and
queue
; Durable Object RPC,
fetch
, and
alarm
; Workflow
run
and external events; local CLI input.
A webhook uses an API route. Internal SDK callbacks and Workflow step callbacks do not create another system entrypoint.
Entrypoint 是外部输入或控制跨越既定系统边界的位置。
明确边界、外部调用方或 runtime,以及接收的操作。 根据调用契约推导 entrypoints,并使用代码定位它们。
示例:Worker
fetch
→ API routes/server functions、
scheduled
和
queue
;Durable Object RPC、
fetch
和
alarm
;Workflow
run
和 external events;local CLI input。
Webhook 使用 API route。 Internal SDK callbacks 和 Workflow step callbacks 不会创建另一个 system entrypoint。

Bootstrap Dependencies

引导依赖

Code entrypoints initialize or register the system, such as
server.ts
or a class constructor. Operational entrypoints receive external invocations.
Build dependencies at the scope that owns their lifetime: invocation, instance, or transaction. Pass them through context or explicit arguments.
For example,
src/server.ts
calls
createAppDeps(env, ctx)
for each HTTP, queue, and scheduled invocation.
infrastructure/app-deps.ts
owns the dependency graph.
Keep construction separate from application policy. Inner services receive dependencies; they do not construct providers or retrieve runtime bindings.
Code entrypoints 初始化或注册系统,例如
server.ts
或 class constructor。 Operational entrypoints 接收外部调用。
在拥有其生命周期的 scope 中构建依赖:invocation、instance 或 transaction。 通过 context 或显式参数传递依赖。
例如,
src/server.ts
为每次 HTTP、queue 和 scheduled invocation 调用
createAppDeps(env, ctx)
。
infrastructure/app-deps.ts
拥有 dependency graph。
保持构造过程与应用策略分离。 内部 services 接收依赖;它们不构造 providers,也不获取 runtime bindings。

DI — AppDeps Composition Root

DI——AppDeps Composition Root

Keep application wiring in
infrastructure/app-deps.ts
; bootstrap code supplies its runtime inputs.
  • createAppDeps(env, ctx)
    builds repos, services, gateways; services lazily memoized with
    once()
    .
  • Bootstrap supplies
    AppDeps
    through request context or the project's composition middleware:
    context.deps.services.messageBus()
    .
  • Handler signature:
    (message, deps: AppDeps) => Promise<TResult>
    .
  • once()
    -memoized services must not capture transaction-scoped repos — they outlive any tx.
  • Every entry is a client for something outside the process — database, queue, Durable Object, Workflow, external API. Group them:
    services
    (clients wrapped in our interfaces, e.g.
    queueClient(): IQueueClient
    ),
    clients
    (raw SDK clients),
    dos
    (Durable Object clients),
    workflows
    . Never a raw binding type in the interface.
  • A rule is not a dep, data is not a dep. A policy object (
    DestinationPolicy
    ,
    RateTable
    , allow-lists, config-fed decisions) belongs in the domain as a pure function; the handler reads its inputs from the database via
    readDb
    and calls it. Named after a message kind (
    commands()
    ) is always wrong — name the capability.
This example assumes the application returns a bare value and uses the existing throw-based contract. For internal Result handlers, use the established adapter conversion described under Error Handling.
typescript
// 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
  })
将 application wiring 保留在
infrastructure/app-deps.ts
;bootstrap code 提供其 runtime inputs。
  • createAppDeps(env, ctx)
    构建 repos、services 和 gateways;services 使用
    once()
    进行惰性记忆化。
  • Bootstrap 通过 request context 或项目的 composition middleware 提供
    AppDeps
    :
    context.deps.services.messageBus()
    。
  • Handler signature:
    (message, deps: AppDeps) => Promise<TResult>
    。
  • 使用
    once()
    记忆化的 services 不得捕获 transaction-scoped repos,因为它们的生命周期会长于任何 tx。
  • 每一项都是进程外部某对象的 client——database、queue、Durable Object、Workflow 或 external API。对其进行分组:
    services
    (由我们的接口包装的 clients,例如
    queueClient(): IQueueClient
    )、
    clients
    (原始 SDK clients)、
    dos
    (Durable Object clients)、
    workflows
    。接口中绝不能使用原始 binding type。
  • Rule 不是 dep,data 也不是 dep。 Policy object(
    DestinationPolicy
    、
    RateTable
    、allow-lists、基于 config 的 decisions)应作为纯函数放在 domain 中;handler 通过
    readDb
    从数据库读取其输入,然后调用它。以 message kind 命名(
    commands()
    )始终是错误的——应按 capability 命名。
此示例假设 application 返回裸值,并使用现有的 throw-based contract。 对于 internal Result handlers,使用 Error Handling 中所述的既有 adapter conversion。
typescript
// 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
  })

Error Handling

错误处理

This skill owns transport contracts, error conversion, logging ownership, and external retries. Use typescript-standards for internal Result types, schemas, and general module design.
Before editing a server function or stub:
  1. Read its caller, shared contracts, and a comparable existing function.
  2. Read
    src/start.ts
    and the registered error, authentication, and CSRF middleware.
  3. Read the error payload module and any installed helper used by that middleware.
  4. Confirm the success type, failure channel, and logging owner.
  5. State material differences between the specification, reference code, and current project.
Read transport contracts for server functions, HTTP, Durable Object RPC, or WebSockets. Read error paths and retries when changing failure behavior or an external client. Read RPC compatibility only when the existing boundary uses a result envelope.
Current server functions return bare values. Shared middleware converts thrown failures to plain tagged payloads once. An internal Result becomes success or a typed throw at the adapter; it does not require a wire envelope. Reuse global middleware registration. Do not install a second error boundary in each function. Preserve Router redirects and not-found control flow through the existing middleware.
For a stub, use the project's unimplemented helper and declare the intended return type. Keep required validation and authentication. Do not fake success or add production behavior outside the requested scope.
本 skill 负责 transport contracts、error conversion、logging ownership 和 external retries。 internal Result types、schemas 和通用 module design 请使用 typescript-standards。
编辑 server function 或 stub 之前:
  1. 阅读其 caller、shared contracts 和一个具有可比性的现有 function。
  2. 阅读
    src/start.ts
    以及已注册的 error、authentication 和 CSRF middleware。
  3. 阅读 error payload module 和该 middleware 使用的任何已安装 helper。
  4. 确认 success type、failure channel 和 logging owner。
  5. 说明 specification、reference code 和 current project 之间的重要差异。
涉及 server functions、HTTP、Durable Object RPC 或 WebSockets 时,阅读 transport contracts。 更改 failure behavior 或 external client 时,阅读 error paths and retries。 仅当现有 boundary 使用 result envelope 时,才阅读 RPC compatibility。
当前 server functions 返回裸值。Shared middleware 一次性将抛出的 failure 转换为带有类型标签的普通 payload。 Internal Result 在 adapter 处转换为 success 或 typed throw;它不需要 wire envelope。 复用 global middleware registration。不要在每个 function 中安装第二层 error boundary。 通过现有 middleware 保留 Router redirects 和 not-found control flow。
对于 stub,使用项目中的 unimplemented helper,并声明预期的 return type。 保留必需的 validation 和 authentication。不要伪造 success,也不要在请求范围之外添加 production behavior。

Entrypoints and Application Orchestration

Entrypoints 与 Application Orchestration

For every task that writes or reviews an entrypoint, application handler, use case, or service adapter, read references/entrypoints-and-application.md before proposing or editing code.
Inspect layers in any useful order. Before accepting the design, trace the complete flow across the entrypoint, application, domain, and infrastructure boundaries.
The application flow must be visible at a glance. A typical handler loads data, prepares or decides, calls a capability, persists, and returns. This is a shape, not a required action count. One-step reads are valid. Deep nesting, repeated branches, or a handler that forwards everything to one broad service are warnings that ownership is unclear.
对于编写或审查 entrypoint、application handler、use case 或 service adapter 的每项任务,在提出或编辑代码之前,都应阅读 references/entrypoints-and-application.md。
可以按任意有用顺序检查各层。在接受设计之前,追踪 entrypoint、application、domain 和 infrastructure boundaries 之间的完整流程。
Application flow 必须一目了然。典型 handler 会加载数据、准备或决策、调用 capability、持久化并返回结果。这是一种形态,而不是规定必须执行的操作数量。One-step reads 同样有效。深层嵌套、重复分支,或将所有内容转发给某个宽泛 service 的 handler,通常意味着 ownership 不清晰。

Entrypoint rule

Entrypoint rule

An operational entrypoint owns boundary work only. Apply the boundary test above before assigning that role.
  • Message-bus architecture: validate and authorize when required, build or parse a
    Command | Query | Event
    , await
    bus.handle(message)
    , then convert the result.
  • Direct-handler architecture: validate and authorize when required, build typed input, await one application handler, then convert the result.
  • Each platform boundary owns its delivery semantics: HTTP responses, queue acknowledgement, or scheduled invocation failures. Workflow orchestration owns durable step configuration.
Operational entrypoint 只负责 boundary work。分配该角色之前,应应用上述 boundary test。
  • **Message-bus architecture:**按需执行 validation 和 authorization,构建或解析
    Command | Query | Event
    ,等待
    bus.handle(message)
    ,然后转换结果。
  • **Direct-handler architecture:**按需执行 validation 和 authorization,构建 typed input,等待一个 application handler,然后转换结果。
  • 每个 platform boundary 拥有自己的 delivery semantics:HTTP responses、queue acknowledgement 或 scheduled invocation failures。Workflow orchestration 拥有 durable step configuration。

Application rule

Application rule

The application handler is the main orchestrator. It owns use-case policy, plans, ordering, port coordination, transaction boundaries, cleanup or compensation, and useful error behavior.
It does not own transport conversion, framework code, provider commands, SQL, SDK calls, or domain invariants.
Use the forwarding-wrapper test: if removing the handler leaves the same broad service call and result, inspect that service. If it coordinates multiple external capabilities, split those capabilities into ports and move the sequence into the handler. Do not invent steps for a genuine one-port read.
Application handler 是主要 orchestrator。它负责 use-case policy、plans、ordering、port coordination、transaction boundaries、cleanup 或 compensation,以及有用的 error behavior。
它不负责 transport conversion、framework code、provider commands、SQL、SDK calls 或 domain invariants。
使用 forwarding-wrapper test:如果移除 handler 后,仍然执行相同的宽泛 service call 并返回相同结果,就应检查该 service。如果它协调多个 external capabilities,应将这些 capabilities 拆分为 ports,并把 sequence 移入 handler。不要为真正的 one-port read 虚构额外步骤。

Business Logic Placement

Business Logic 的位置

LayerBusiness logic?
Entrypoint❌ parse, authorize, dispatch, respond
HandlerOrchestration policy only
Domain model✅ invariants, state transitions
Domain service✅ cross-entity algorithms
Repository❌ persistence only
Escape hatch ("two gates"): an entrypoint may call a dedicated service client directly when the operation is transport coordination. The client must expose one narrow capability. Inline use-case logic in entrypoints is never allowed.

层包含 Business logic?
Entrypoint❌ 解析、授权、分发、响应
Handler仅负责编排策略
Domain model✅ invariants、state transitions
Domain service✅ 跨 entity algorithms
Repository❌ 仅负责 persistence
**Escape hatch(“two gates”):**当操作属于 transport coordination 时,entrypoint 可以直接调用专用 service client。该 client 必须只暴露一项狭窄 capability。绝不允许在 entrypoint 中内联 use-case logic。

Server Mental Model — Every Entrypoint Has the Same Role

Server Mental Model——每个 Entrypoint 都扮演相同角色

Message bus:    Entrypoint → Command | Query | Event → Bus → Handler → Domain + Ports
Direct handler: Entrypoint → Typed input → Handler → Domain + Ports
An identified operational entrypoint dispatches into the application and converts the result for its external contract. The transport is incidental.
Rules:
  1. Entrypoints are thin: validate when required → authorize when required → dispatch → convert.
  2. Messages are pure data. No methods.
  3. Handlers orchestrate. Zero business logic in handlers.
  4. Domain is pure. No framework or infrastructure imports.
  5. Dependencies flow inward — never the reverse.
  6. Repository pattern: interface in domain, implementation in infrastructure.
Message bus:    Entrypoint → Command | Query | Event → Bus → Handler → Domain + Ports
Direct handler: Entrypoint → Typed input → Handler → Domain + Ports
已识别的 operational entrypoint 将请求分派到 application,并将结果转换为对应的 external contract。Transport 本身是次要因素。
规则:
  1. Entrypoints 保持轻量:按需 validation → 按需 authorization → dispatch → convert。
  2. Messages 是纯 data,不包含 methods。
  3. Handlers 负责 orchestration,其中不包含 business logic。
  4. Domain 保持纯粹,不导入 framework 或 infrastructure。
  5. 依赖只能向内流动,绝不能反向流动。
  6. Repository pattern:interface 位于 domain,implementation 位于 infrastructure。

Messages — Command / Event / Query

Messages——Command / Event / Query

TypeHandlersIdempotencySemantics
Commandexactly 1
id
claimed in UoW
mutation
Event0..N subscribersper-subscriber claimdomain fact, fan-out
Queryexactly 1none — no
id
read, bypasses UoW
Base message facts:
  • type: 'command' | 'event' | 'query'
    discriminant +
    name
    literal + optional
    userId
    (observability).
  • id
    is
    uuid | externalMessageId
    . External ids are namespaced provider keys —
    stripe:event:evt_123
    — giving provider-keyed idempotency for webhooks. Factories (
    createCommand(...)
    ) default the uuid.
    [detect base.messages.ts]
    : current generation uses
    z.uuidv7()
    (time-ordered); older uses
    z.uuidv4()
    .
  • [detect]
    message location: shared with client →
    packages/core/src/domain/**
    ; server-only →
    apps/<app>/src/server/domain/messages/
    .
  • Payload IDs use branded schemas (
    UserIdSchema
    ,
    ProductIdSchema
    ); Zod v4 helpers (
    z.uuid()
    ,
    z.uuidv7()
    ,
    z.iso.datetime()
    ).
typescript
export const createProductSchema = baseCommandSchema.extend({
  name: z.literal('CreateProduct'),
  productName: z.string(),
  userId: UserIdSchema,
})
类型Handlers幂等性语义
Command恰好 1 个在 UoW 中 claim
id
mutation
Event0..N 个 subscribers每个 subscriber 单独 claimdomain fact、fan-out
Query恰好 1 个无——不需要
id
read,绕过 UoW
基础 message facts:
  • type: 'command' | 'event' | 'query'
    discriminant +
    name
    literal + optional
    userId
    (用于 observability)。
  • id
    是
    uuid | externalMessageId
    。External ids 是带命名空间的 provider keys,例如
    stripe:event:evt_123
    ,从而为 webhooks 提供以 provider key 为依据的幂等性。Factories(
    createCommand(...)
    )默认使用 uuid。
    [detect base.messages.ts]
    :当前 generation 使用
    z.uuidv7()
    (时间有序);旧 generation 使用
    z.uuidv4()
    。
  • [detect]
    message location:与 client 共享 →
    packages/core/src/domain/**
    ;仅限 server →
    apps/<app>/src/server/domain/messages/
    。
  • Payload IDs 使用 branded schemas(
    UserIdSchema
    、
    ProductIdSchema
    );Zod v4 helpers(
    z.uuid()
    、
    z.uuidv7()
    、
    z.iso.datetime()
    )。
typescript
export const createProductSchema = baseCommandSchema.extend({
  name: z.literal('CreateProduct'),
  productName: z.string(),
  userId: UserIdSchema,
})

Registry

Registry

One registry file (
application/registry/registry.ts
or
application/handlers/registry.ts
) wires names to handlers;
satisfies CommandRegistry / QueryRegistry
makes the compiler prove exhaustiveness. If the compiler doesn't error when you add a message, it isn't wired.
typescript
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 },
  ],
}
使用一个 registry file(
application/registry/registry.ts
或
application/handlers/registry.ts
)将 names 连接到 handlers;
satisfies CommandRegistry / QueryRegistry
让编译器证明 exhaustiveness。如果添加 message 时编译器没有报错,就说明它尚未完成 wiring。
typescript
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 vs effect subscribers — the load-bearing dispatch rule

State 与 effect subscribers——决定成败的分发规则

The species test for every event subscriber: does it write our DB?
KindRuns inIdempotencyFor
state
its own per-subscriber UoWreceipt keyed by event id (+ subscriber id)DB writes: projections, denormalizations
effect
no tx, no claimidempotency key delegated to the target systemanalytics, email, external APIs
A failing
state
subscriber rolls back only its own UoW. An
effect
subscriber must be safe to re-run — never wrap an effect in a DB claim (claim-then-effect loses the effect on crash; effect-then-claim leaves an orphan window). A subscriber that does both → split it.
[detect registry.ts]
wiring: newer generation registers subscriber objects
{ id, mode, handle }
and the dispatch machinery opens each state subscriber's UoW; older generation registers plain
(event, deps)
functions, each state subscriber opening its own UoW keyed off the event id.
对每个 event subscriber 都要进行 species test:它是否写入我们的 DB?
Kind运行位置幂等性用途
state
每个 subscriber 自己的 UoWreceipt 以 event id(以及 subscriber id)为 keyDB writes:projections、denormalizations
effect
无 tx、无 claimidempotency key 委托给 target systemanalytics、email、external APIs
失败的
state
subscriber 仅回滚自己的 UoW。
effect
subscriber 必须能够安全地重新运行——绝不能用 DB claim 包裹 effect(claim-then-effect 会在崩溃时丢失 effect;effect-then-claim 会留下 orphan window)。同时执行两种操作的 subscriber 应拆分为两个。
[detect registry.ts]
wiring:较新的 generation 注册 subscriber objects
{ id, mode, handle }
,dispatch machinery 为每个 state subscriber 打开自己的 UoW;较旧的 generation 注册普通
(event, deps)
functions,每个 state subscriber 自行打开以 event id 为 key 的 UoW。

Message Bus

Message Bus

Single choke point from typed message to handler; the bus routes, it does not open transactions. Event subscribers run in parallel (
Promise.allSettled
) — one failure doesn't abort siblings. Duplicate handling:
DuplicateMessageError
from an event subscriber → swallowed as success; from a command → rethrown to the caller; the queue consumer acks duplicates as skip.
从 typed message 到 handler 的唯一 choke point;bus 负责 routing,但不打开 transactions。Event subscribers 并行运行(
Promise.allSettled
),一个失败不会中止其他 subscribers。重复处理:event subscriber 抛出
DuplicateMessageError
时吞掉并视为成功;command 抛出时重新抛给 caller;queue consumer 将 duplicates 作为 skip 进行 ack。

Unit of Work

Unit of Work

Command handlers open the UoW (not the bus):
deps.uow.run(messageId, (repos, emit) => …)
— tx-scoped repos plus an
emit
for domain events. Event
state
subscribers get their own per-subscriber UoW (opened per the generation's wiring, above). One atomic scope does, in order:
  1. Claim receipt — insert
    message.id
    into
    message_receipts
    ; conflict →
    DuplicateMessageError
    .
  2. Handler writes.
  3. Persist emitted events to
    message_outbox
    — state change and events commit or roll back together.
  4. Commit.
[detect unit-of-work implementation]
:
  • D1: no interactive transactions — receipt + writes + outbox staged into one
    db.batch()
    , committed with retry; receipt UNIQUE violation →
    DuplicateMessageError
    .
  • Postgres: real tx; Result-based with a rollback bridge (store
    Result.err
    →
    tx.rollback()
    → return stored err), branded
    Tx
    type, events collected via AsyncLocalStorage.
Transaction rules:
  • No cross-boundary I/O inside a tx — no fetch/queue/R2 while holding a connection (Hyperdrive pool exhaustion).
  • One aggregate per tx. Retry the whole UoW at the caller boundary. Never nest UoWs.
  • Queries bypass UoW entirely; the read path may skip repositories — query handlers can do inline Drizzle
    select({...})
    → DTO. The DTO is a product resource, not a row. Do not leak a storage accident. Keep expensive associations off unless the caller asks.
Command handlers 打开 UoW,而不是 bus:
deps.uow.run(messageId, (repos, emit) => …)
,其中包含 tx-scoped repos 和用于 domain events 的
emit
。Event
state
subscribers 获得各自的 per-subscriber UoW(按照上述 generation 的 wiring 方式打开)。一个 atomic scope 按顺序执行:
  1. Claim receipt——将
    message.id
    插入
    message_receipts
    ;发生 conflict 时抛出
    DuplicateMessageError
    。
  2. Handler writes。
  3. 将 emitted events 持久化到
    message_outbox
    ——state change 与 events 要么一起提交,要么一起回滚。
  4. Commit。
[detect unit-of-work implementation]
:
  • D1:不支持 interactive transactions——receipt、writes 和 outbox 被暂存到一次
    db.batch()
    中,并通过 retry 提交;receipt UNIQUE violation 转换为
    DuplicateMessageError
    。
  • Postgres:使用真实 tx;基于 Result,并通过 rollback bridge 实现(存储
    Result.err
    →
    tx.rollback()
    → 返回存储的 err),使用 branded
    Tx
    type,并通过 AsyncLocalStorage 收集 events。
Transaction rules:
  • Tx 内不得执行跨 boundary I/O——持有 connection 时不得执行 fetch/queue/R2,以免耗尽 Hyperdrive pool。
  • 每个 tx 只处理一个 aggregate。在 caller boundary 重试整个 UoW。绝不嵌套 UoW。
  • Queries 完全绕过 UoW;read path 可以跳过 repositories——query handlers 可以直接使用 Drizzle
    select({...})
    → DTO。DTO 是 product resource,而不是 row。不得泄露 storage accident。除非 caller 明确要求,否则不要加载昂贵的 associations。

Outbox relay

Outbox relay

Events in
message_outbox
are delivered by a relay, not by the request: a sweep (cron
*/1 * * * *
, or a relay DO alarm draining into Queues —
[detect wrangler.jsonc]
). Post-commit
deps.hooks.onEventsCommitted()
wakes the relay via
waitUntil
as a latency optimization only — the sweep is the source of truth; a lost wake loses no events.
message_outbox
中的 events 由 relay 投递,而不是由 request 投递:使用 sweep(cron
*/1 * * * *
,或 relay DO alarm 排入 Queues——
[detect wrangler.jsonc]
)。Commit 后的
deps.hooks.onEventsCommitted()
通过
waitUntil
唤醒 relay,这仅是 latency optimization;sweep 才是 source of truth;丢失一次 wake 不会丢失 events。

Idempotency

幂等性

Everything that mutates must be idempotent. Three layers:
  1. Message receipts — automatic via UoW (above). Safe to retry commands / redeliver events.
  2. External side effects — pass idempotency keys derived from stable operation properties:
    typescript
    await stripe.charges.create({ amount }, { idempotencyKey: `${runId}-${nodeId}` })
    Webhook-driven messages reuse the provider's key as the message id (
    stripe:event:evt_123
    ).
  3. DB level — atomic insert, not check-then-insert:
    .onConflictDoNothing()
    /
    .onConflictDoUpdate()
    . No TOCTOU window.
所有会改变状态的操作都必须具备幂等性,分为三层:
  1. Message receipts——通过 UoW 自动实现(如上)。可安全重试 commands 或重新投递 events。
  2. External side effects——传递由稳定 operation properties 派生的 idempotency keys:
    typescript
    await stripe.charges.create({ amount }, { idempotencyKey: `${runId}-${nodeId}` })
    Webhook 驱动的 messages 复用 provider key 作为 message id(
    stripe:event:evt_123
    )。
  3. DB level——使用 atomic insert,而不是 check-then-insert:
    .onConflictDoNothing()
    /
    .onConflictDoUpdate()
    。避免 TOCTOU window。

Repositories

Repositories

Writes and entity reads go through repositories.
  • Closed method list:
    byId
    /
    findByX
    /
    insert
    /
    save
    /
    delete
    . No generic
    update(id, fields)
    .
  • insert
    ≠
    save
    : insert fails on existing (
    AlreadyExists
    ); save is version-guarded (
    RevisionConflictError
    on stale version).
  • Choose the insert type for each aggregate before you write the handler. Never check, then insert: two concurrent requests both pass the check.
    • Created once: a repeat is a mistake.
      insert
      fails with
      AlreadyExists
      , and the caller reports it.
    • Natural key, repeatable arrival: the same input must return the same aggregate, such as a file identified by its hash.
      insert
      runs
      ON CONFLICT (key) DO NOTHING RETURNING id
      , then reads the existing ID. It returns the ID and dispatches events only for a new row. The handler returns the ID and does not report "new" or "existing".
  • Each repository defines one
    toDomain(row: DbRow): Entity
    mapper for persisted entities.
  • toDomain
    builds the entity's plain data and passes it to
    Entity.restore(data)
    .
  • toDomain
    maps field names and flat database rows to nested plain data. It does not validate or parse values.
  • When flat database nullability conflicts with a domain union, use a narrow cast before
    restore
    ; the constructor enforces domain invariants.
  • Writes map
    entity.toPlainObject()
    to the database insert or update shape inside the repository.
  • Migrate old database values. Do not keep old-value conversion in
    toDomain
    after the migration.
  • Drizzle, never raw SQL in handlers. Raw SQL only when Drizzle can't express the query — documented in the repo.
typescript
export class ProductRepository implements IProductRepository {
  async byId(id: ProductId, userId: UserId) {
    const row = await this.db.query.product.findFirst({
      where: and(eq(product.id, id), eq(product.userId, userId)),
    })
    if (!row) return Result.err(new EntityNotFoundError('Product', id))
    return Result.ok(this.toDomain(row))
  }

  private toDomain(row: DbProduct): Product {
    return Product.restore({
      id: row.id,
      name: row.name,
      state: { status: row.status },
    })
  }
}
Drizzle table definitions use the array return for indexes:
typescript
sqliteTable('t', { ... }, (t) => [index('idx').on(t.col)])
Writes 和 entity reads 都通过 repositories 完成。
  • 封闭的方法列表:
    byId
    /
    findByX
    /
    insert
    /
    save
    /
    delete
    。不要使用通用的
    update(id, fields)
    。
  • insert
    ≠
    save
    :insert 在实体已存在时失败(
    AlreadyExists
    );save 使用 version guard(版本过旧时抛出
    RevisionConflictError
    )。
  • 编写 handler 前,先为每个 aggregate 选择 insert type。绝不能先检查再 insert,否则两个并发 request 都可能通过检查。
    • **仅创建一次:**重复创建属于错误。
      insert
      失败并返回
      AlreadyExists
      ,由 caller 报告该错误。
    • **Natural key、可重复到达:**相同输入必须返回同一个 aggregate,例如由 hash 标识的文件。
      insert
      执行
      ON CONFLICT (key) DO NOTHING RETURNING id
      ,然后读取已有 ID。它返回 ID,并且仅为新 row 分发 events。Handler 返回 ID,不报告“new”或“existing”。
  • 每个 repository 为持久化 entities 定义一个
    toDomain(row: DbRow): Entity
    mapper。
  • toDomain
    构建 entity 的 plain data,并将其传给
    Entity.restore(data)
    。
  • toDomain
    将字段名以及 flat database rows 映射为 nested plain data。它不验证或解析 values。
  • 当 flat database nullability 与 domain union 冲突时,在
    restore
    前使用 narrow cast;constructor 负责强制执行 domain invariants。
  • Writes 在 repository 内将
    entity.toPlainObject()
    映射为 database insert 或 update shape。
  • 迁移旧 database values。迁移后,不要继续在
    toDomain
    中保留 old-value conversion。
  • **使用 Drizzle,handlers 中绝不写 raw SQL。**仅当 Drizzle 无法表达 query 时才使用 raw SQL,并应在 repo 中记录说明。
typescript
export class ProductRepository implements IProductRepository {
  async byId(id: ProductId, userId: UserId) {
    const row = await this.db.query.product.findFirst({
      where: and(eq(product.id, id), eq(product.userId, userId)),
    })
    if (!row) return Result.err(new EntityNotFoundError('Product', id))
    return Result.ok(this.toDomain(row))
  }

  private toDomain(row: DbProduct): Product {
    return Product.restore({
      id: row.id,
      name: row.name,
      state: { status: row.status },
    })
  }
}
Drizzle table definitions 使用 array return 定义 indexes:
typescript
sqliteTable('t', { ... }, (t) => [index('idx').on(t.col)])

Domain Entities

Domain Entities

Entity pattern only where there's identity + state transitions + invariants; plain types otherwise.
  • Base
    Entity
    :
    addEvent
    /
    collectEvents
    /
    clearEvents
    ; serialization via
    toPlainObject()
    .
  • A persisted entity exposes
    toPlainObject(): EntityData
    and
    static restore(data: EntityData): Entity
    .
  • restore
    accepts the exact type that
    toPlainObject
    returns.
  • Use a private constructor. Validate key business invariants in the constructor or in one shared assertion that it calls.
  • Do not repeat Zod validation from HTTP or I/O boundaries in the domain model.
  • Do not repeat database type, nullability, or constraint checks in the domain model.
  • Domain assertions augment boundary validation with rules that the type and database schema cannot express.
  • create
    ,
    restore
    , and state transitions must use the same invariant validation.
  • restore
    rebuilds domain state without emitting domain events.
  • Domain entities must not import database types or expose
    fromPersistence
    and
    toPersistence
    methods.
  • static create(...)
    — current generation returns
    Result<Entity, ValidationError>
    (throw = bug only); older generation throws on invariant violation.
typescript
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 }
  }
}
Mutations guard their transitions:
typescript
markLive(): Result<void, PlaybookEditError> {
  if (this.status === 'archived') return Result.err(new PlaybookEditError('archived'))
  this.status = 'live'
  this.updatedAt = new Date()
  return Result.ok()
}
仅在对象同时具有 identity、state transitions 和 invariants 时使用 Entity pattern;其他情况使用 plain types。
  • Base
    Entity
    :
    addEvent
    /
    collectEvents
    /
    clearEvents
    ;通过
    toPlainObject()
    序列化。
  • Persisted entity 暴露
    toPlainObject(): EntityData
    和
    static restore(data: EntityData): Entity
    。
  • restore
    接受
    toPlainObject
    返回的精确类型。
  • 使用 private constructor。在 constructor 或其调用的一个 shared assertion 中验证关键 business invariants。
  • 不要在 domain model 中重复 HTTP 或 I/O boundary 的 Zod validation。
  • 不要在 domain model 中重复 database type、nullability 或 constraint checks。
  • Domain assertions 用于补充 boundary validation,涵盖 type 和 database schema 无法表达的规则。
  • create
    、
    restore
    和 state transitions 必须使用同一套 invariant validation。
  • restore
    重建 domain state,但不发出 domain events。
  • Domain entities 不得导入 database types,也不得暴露
    fromPersistence
    和
    toPersistence
    methods。
  • static create(...)
    ——当前 generation 返回
    Result<Entity, ValidationError>
    (throw 代表 bug);旧 generation 在违反 invariant 时抛出异常。
typescript
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 }
  }
}
Mutations 必须保护其 transitions:
typescript
markLive(): Result<void, PlaybookEditError> {
  if (this.status === 'archived') return Result.err(new PlaybookEditError('archived'))
  this.status = 'live'
  this.updatedAt = new Date()
  return Result.ok()
}

/core
Package Boundary

/core
Package Boundary

packages/core
has two entry points —
index.ts
(full, server) and
client.ts
(browser-safe). Browser code imports
@<scope>/core/client
ONLY; the subpath is the firewall against pulling server-only modules into the bundle.
Core contains browser-safe contracts and domain types, plus explicitly separated server infrastructure where the project uses it. A wire-envelope module is required only by an existing envelope contract. DB schema and migrations live in the app (
apps/<app>/src/server/infrastructure/{db|persistence}/
), not in core.
packages/core
有两个 entry points——
index.ts
(完整、server-side)和
client.ts
(browser-safe)。Browser code 只能导入
@<scope>/core/client
;该 subpath 是防止 server-only modules 进入 bundle 的 firewall。
Core 包含 browser-safe contracts 和 domain types,以及项目使用时的明确分离的 server infrastructure。只有现有 envelope contract 才需要 wire-envelope module。DB schema 和 migrations 位于 app 中(
apps/<app>/src/server/infrastructure/{db|persistence}/
),而不是 core 中。

Published contracts

Published contracts

Classify the surface before you change a shape.
SurfaceExamplesBreak rule
InternalTanStack server functions that ship with the web clientCoordinated break allowed in the same deploy
PublishedMCP tool input/output, extension contract, HTTP
/api
, outbound webhook payload,
@<scope>/core/client
schemas other clients import
Overlap, warn, then remove
A published shape may change. It may not vanish in the same change that replaces it.
  1. Ship the new field, tool, or shape next to the old one.
  2. Mark the old one deprecated where a machine can see it:
    Deprecation
    +
    Sunset
    on HTTP,
    deprecated: true
    on an MCP tool or argument,
    @deprecated
    on an exported symbol.
  3. State when it goes away (date, release, or no traffic for N days).
  4. After that window, delete the old one in its own change.
Keep one business core. Fill both shapes from the same handler. Do not fork
/v1/
handlers. Do not keep dual fields forever.
A silent rename is forbidden. Keep the old path only if a client you cannot reach still depends on it.
Published rules:
  • Consumers must ignore unknown fields.
  • The DTO hides storage accidents. Do not publish a persistence shape.
  • Expensive associations are off by default. The caller opts in.
  • Any operation exposed to code (MCP, HTTP, extension) needs a rate limit. Tighter limits on expensive work. Return
    Retry-After
    on 429. Keep a per-customer kill switch.
更改 shape 之前,先对 surface 进行分类。
Surface示例Break rule
Internal随 web client 一起发布的 TanStack server functions可在同一次 deploy 中协调进行 breaking change
PublishedMCP tool input/output、extension contract、HTTP
/api
、outbound webhook payload、其他 clients 导入的
@<scope>/core/client
schemas
先 overlap、warn,再 remove
Published shape 可以更改,但不能在替换它的同一次更改中消失。
  1. 将新 field、tool 或 shape 与旧版本一起发布。
  2. 以机器可识别的方式将旧版本标记为 deprecated:HTTP 使用
    Deprecation
    +
    Sunset
    ,MCP tool 或 argument 使用
    deprecated: true
    ,exported symbol 使用
    @deprecated
    。
  3. 说明旧版本何时移除(日期、release,或 N 天内无 traffic)。
  4. 经过该窗口后,在单独的 change 中删除旧版本。
保持单一 business core。两个 shapes 都应从同一个 handler 填充。不要 fork
/v1/
handlers。永远不要长期保留 dual fields。
禁止 silent rename。只有在仍无法触达的 client 依赖旧路径时,才保留该路径。
Published rules:
  • Consumers 必须忽略 unknown fields。
  • DTO 隐藏 storage accidents。不得发布 persistence shape。
  • Expensive associations 默认关闭,由 caller 主动选择启用。
  • 所有暴露给代码的操作(MCP、HTTP、extension)都需要 rate limit。昂贵工作应使用更严格的 limits。返回 429 时提供
    Retry-After
    。保留 per-customer kill switch。

Pagination

Pagination

  • Small user-scoped lists:
    ?page=1&limit=20
    (max 100) →
    { items, page, limit, total, hasNextPage, hasPrevPage }
    .
  • Unbounded collections (job history, audit, MCP list tools): cursor +
    next
    token. Query is
    WHERE id > cursor ORDER BY id LIMIT n
    . Never offset those lists.

  • Small user-scoped lists:
    ?page=1&limit=20
    (最大 100)→
    { items, page, limit, total, hasNextPage, hasPrevPage }
    。
  • Unbounded collections(job history、audit、MCP list tools):使用 cursor +
    next
    token。Query 为
    WHERE id > cursor ORDER BY id LIMIT n
    。这些 lists 绝不使用 offset。

Platform Patterns

Platform Patterns

Queue consumers

Queue consumers

Class-based consumer in
entrypoints/queue/
:
  • Per-message Sentry isolation scope.
  • classifyForRetry
    — typed errors decide retry vs park; explicit exponential backoff with caps.
  • max_retries
    + DLQ configured in wrangler.jsonc; poison messages parked; duplicates acked as skip.
entrypoints/queue/
中采用 class-based consumer:
  • Per-message Sentry isolation scope。
  • classifyForRetry
    ——由 typed errors 决定 retry 或 park;使用带上限的明确 exponential backoff。
  • 在 wrangler.jsonc 中配置
    max_retries
    + DLQ;poison messages 被 parked;duplicates 作为 skip 进行 ack。

Cron / scheduled

Cron / scheduled

  • Cron registry (
    entrypoints/scheduled/cron-registry.ts
    ) maps expressions → handlers; includes the outbox relay sweep.
  • Cron does not retry. Each unit of work catches its own errors; one bad item must not kill the sweep.
  • Local trigger:
    curl "http://localhost:8787/cdn-cgi/handler/scheduled"
    .
  • Cron registry(
    entrypoints/scheduled/cron-registry.ts
    )将 expressions 映射到 handlers;其中包括 outbox relay sweep。
  • **Cron 不会 retry。**每个 unit of work 自行捕获 errors;一个 bad item 不能终止整个 sweep。
  • Local trigger:
    curl "http://localhost:8787/cdn-cgi/handler/scheduled"
    。

Cloudflare Workflows

Cloudflare Workflows

  • One unit of work per step — steps are the retry boundary.
  • Fetch-then-record: external call in one step, outcome recorded in the next; a retried step must never redo a committed effect.
  • Intent → effect → outcome: mint durable intent (DB row) before the external effect.
  • Map non-retryable typed errors to
    NonRetryableError
    ; internal vs external step configs carry the retry policies.
  • 每个 step 对应一个 unit of work——step 是 retry boundary。
  • **Fetch-then-record:**在某个 step 中执行 external call,在下一个 step 中记录结果;重试某个 step 时,绝不能重复执行已经 commit 的 effect。
  • Intent → effect → outcome:在执行 external effect 之前,先创建 durable intent(DB row)。
  • 将 non-retryable typed errors 映射为
    NonRetryableError
    ;internal 和 external step configs 分别携带 retry policies。

Durable Objects

Durable Objects

In use: rate limiting (
RateLimiterDO
), outbox relay, agents/gates (Agents SDK), container supervisors. Wrap with
Sentry.instrumentDurableObjectWithSentry(...)
.
Migrations: each change = new migration, unique tag, append-only — never edit or remove shipped entries. Default
new_sqlite_classes
for new DOs;
new_classes
only with a documented reason (legacy DOs exist — don't migrate them unprompted). Use the new class name in code after rename; never manually create destination classes for rename/transfer.
jsonc
"migrations": [
  { "tag": "v1", "new_sqlite_classes": ["SessionDO"] },
  { "tag": "v2", "renamed_classes": [{"from": "OldDO", "to": "NewDO"}] }
]
使用场景包括 rate limiting(
RateLimiterDO
)、outbox relay、agents/gates(Agents SDK)和 container supervisors。使用
Sentry.instrumentDurableObjectWithSentry(...)
包装。
Migrations:每次 change 都创建新 migration,使用唯一 tag,并且只能追加——绝不编辑或删除已经发布的 entries。新 DOs 默认使用
new_sqlite_classes
;仅在有文档说明的原因时使用
new_classes
(legacy DOs 已存在时,不要在未要求时迁移它们)。Rename 后应在代码中使用新 class name;rename/transfer 时,绝不能手动创建 destination classes。
jsonc
"migrations": [
  { "tag": "v1", "new_sqlite_classes": ["SessionDO"] },
  { "tag": "v2", "renamed_classes": [{"from": "OldDO", "to": "NewDO"}] }
]

Containers

Containers

Reference usage: a Python service (uv/pyproject) in its own app dir; the Start app's wrangler
containers
entry points at its Dockerfile with a supervising DO (
class_name
), Sentry-instrumented. Linux/amd64 only; images upload to Cloudflare Registry.
参考用法:独立 app dir 中的 Python service(uv/pyproject);Start app 的 wrangler
containers
entry 指向其 Dockerfile,并通过 supervising DO(
class_name
)管理,且已接入 Sentry instrumentation。仅支持 Linux/amd64;images 上传到 Cloudflare Registry。

Workers KV

Workers KV

Before adding, reviewing, or tuning a KV key, read workers KV: long
cacheTtl
on keys that never change, versioned keys, shared keys, one read per request, write at publish time.
添加、审查或调优 KV key 之前,阅读 workers KV:对永不变化的 keys 使用较长的
cacheTtl
,使用 versioned keys 和 shared keys,每个 request 只读取一次,并在 publish time 写入。

Remote bindings

Remote bindings

"remote": true
on a binding connects local dev to deployed resources (billing applies). Supported: AI, D1, KV, R2, Queues, Vectorize, Workflows. Local-only: Durable Objects, Hyperdrive, Rate Limiting. Vitest always uses local simulations.
Binding 上的
"remote": true
会将 local dev 连接到 deployed resources(会产生费用)。支持 AI、D1、KV、R2、Queues、Vectorize 和 Workflows。仅限 local:Durable Objects、Hyperdrive 和 Rate Limiting。Vitest 始终使用 local simulations。

Typegen

Typegen

Run the project's
cf-typegen
script after configuration or binding changes. Never edit generated types. Plain
wrangler types
combines configured environments into strict unions and emits
Cloudflare.<EnvName>Env
. Use
--env
only when generating for one environment intentionally. Never hide differences with
--strict-vars=false
. For cross-Worker RPC, pass the caller and every bound Worker configuration with repeated
-c
flags. Keep required binding names in each environment so Wrangler resolves
Service<typeof Entrypoint>
without casts.
更改 configuration 或 bindings 后,运行项目的
cf-typegen
script。绝不编辑 generated types。 Plain
wrangler types
会合并 configured environments,形成 strict unions,并生成
Cloudflare.<EnvName>Env
。 仅在有意为单个 environment 生成时使用
--env
。绝不能通过
--strict-vars=false
隐藏差异。 进行跨 Worker RPC 时,使用重复的
-c
flags 传入 caller 及每个绑定的 Worker configuration。 在每个 environment 中保留必需的 binding names,以便 Wrangler 无需 casts 即可解析
Service<typeof Entrypoint>
。

Local Explorer API

Local Explorer API

Cloudflare Vite exposes local resources at
http://localhost:<project-port>/cdn-cgi/explorer/api
. Discover the active project URL from configuration or process output. Never assume a shared port. Fetch the OpenAPI schema from that API root before using it. Use only documented operations.
Read the
cloudflare
,
durable-objects
,
wrangler
, or
workers-best-practices
skill for platform changes in its scope.
Cloudflare Vite 会在
http://localhost:<project-port>/cdn-cgi/explorer/api
暴露 local resources。 从 configuration 或 process output 中发现当前 project URL。绝不能假设使用共享 port。 使用该 API 前,先从 API root 获取 OpenAPI schema。仅使用有文档说明的 operations。
涉及相应范围内的 platform changes 时,阅读
cloudflare
、
durable-objects
、
wrangler
或
workers-best-practices
skill。

Deployment

Deployment

Workers, never Pages. TanStack Start apps use
@cloudflare/vite-plugin
:
CLOUDFLARE_ENV=<env> vite build
resolves the env from
wrangler.jsonc
and generates a flattened
dist/server/wrangler.json
with computed
main
/
assets
— those fields are not hand-written.
Env names and deploy scripts are per-project — read
wrangler.jsonc
+
package.json
(e.g.
dev/preview/prod
,
dev/staging/test/prod
;
pnpm deploy
targets the pre-prod env,
pnpm deploy:prod
targets prod, both run migrations first).

**使用 Workers,绝不使用 Pages。**TanStack Start apps 使用
@cloudflare/vite-plugin
:
CLOUDFLARE_ENV=<env> vite build
会从
wrangler.jsonc
解析 env,并生成 flattened
dist/server/wrangler.json
,其中自动计算
main
/
assets
——这些字段不能手工编写。
Env names 和 deploy scripts 因项目而异——阅读
wrangler.jsonc
+
package.json
(例如
dev/preview/prod
、
dev/staging/test/prod
;
pnpm deploy
指向 pre-prod env,
pnpm deploy:prod
指向 prod,两者都会先运行 migrations)。

Database Workflow

Database Workflow

Before a schema change, migration, or database query, read database workflow. It covers D1 and Postgres detection, migration commands, migration rules, and constraint ownership.
进行 schema change、migration 或 database query 之前,阅读 database workflow。其中涵盖 D1 和 Postgres detection、migration commands、migration rules 和 constraint ownership。

Testing

Testing

Before you write, review, or speed up a Worker, binding, or database test, read Workers testing.
编写、审查或加速 Worker、binding 或 database test 之前,阅读 Workers testing。

Security and operations

Security and operations

For Turnstile setup, repair, or CAPTCHA migration, use the official Turnstile Spin skill first. Start with its agent workflow, not manual setup or the dashboard.
For authentication, authorization, public inputs, uploads, or secret handling, read security. For backend performance, logging, metrics, or alerts, read operations.
设置、修复或迁移 Turnstile 和 CAPTCHA 时,首先使用官方 Turnstile Spin skill。 从其 agent workflow 开始,而不是手动设置或使用 dashboard。
涉及 authentication、authorization、public inputs、uploads 或 secret handling 时,阅读 security。 涉及 backend performance、logging、metrics 或 alerts 时,阅读 operations。

Secrets — dotenvx

Secrets——dotenvx

  • Encrypted
    .env
    files are committed; private keys (
    .env.keys
    ) are gitignored. Scripts run under
    dotenvx run --
    .
  • wrangler
    secrets.required
    manifests declare what prod needs.
  • This is the secrets mechanism for these projects — it supersedes
    .dev.vars
    .

加密后的
.env
files 会被提交;private keys(
.env.keys
)会被 gitignore。Scripts 在
dotenvx run --
下运行。 wrangler
secrets.required
manifests 用于声明 prod 所需内容。 这是这些项目的 secrets mechanism,优先于
.dev.vars
。

Standards Not Yet Practiced (apply to new code; don't churn existing)

尚未落实的标准(应用于新代码;不要改动现有代码)

  • List pagination on new list endpoints. Use the Published contracts rule: offset envelope for small user-scoped lists; cursor +
    next
    for unbounded collections.
  • Repository interfaces in domain.
  • Branded IDs on every message payload (legacy payloads have plain strings).
  • List pagination 用于新的 list endpoints。遵循 Published contracts rule:small user-scoped lists 使用 offset envelope;unbounded collections 使用 cursor +
    next
    。
  • Repository interfaces 放在 domain 中。
  • 每个 message payload 都使用 branded IDs(legacy payloads 使用 plain strings)。

MCP Server Config

MCP Server Config

MCP tool input and output schemas are published contracts. Change them with overlap, warn, then remove.
bash
claude mcp add --transport http <name> <url> --header "Authorization: Bearer <token>"
Windows npx servers need the
cmd /c
wrapper:
json
{ "command": "cmd", "args": ["/c", "npx", "-y", "@package/name"] }
MCP tool input 和 output schemas 是 published contracts。更改时遵循 overlap、warn、then remove 的流程。
bash
claude mcp add --transport http <name> <url> --header "Authorization: Bearer <token>"
Windows npx servers 需要
cmd /c
wrapper:
json
{ "command": "cmd", "args": ["/c", "npx", "-y", "@package/name"] }