
React Starter Kit 边缘架构实战Cloudflare Workers 服务绑定、Hyperdrive 数据库代理与 Terraform 基础设施全解析【免费下载链接】react-starter-kitModern React starter kit with Bun, TypeScript, Tailwind CSS, tRPC, Stripe, and Cloudflare Workers. Production-ready monorepo for building fast web apps.项目地址: https://gitcode.com/gh_mirrors/rea/react-starter-kitReact Starter Kit 将整个应用拆分为三个部署在 Cloudflare Workers 上的独立服务——web边缘路由器 营销站点、appSPA 静态资源、apiHono/tRPC/Better Auth 后端并以 service bindings 在 Cloudflare 内网直接互联。本文以仓库的 边缘架构文档 为主体结合各 Worker 真实的wrangler.jsonc配置、web worker 路由源码、Hyperdrive 连接实现 与 Terraform 基础设施完整讲解这套边缘拓扑的配置方法、关键陷阱与本地开发方案。读完你将能独立配置三服务绑定、理解 Hyperdrive 双绑定缓存策略、复现基于认证提示 Cookie 的/路由并掌握 Wrangler 与 Terraform 的职责边界。一、架构背景三个 Worker 组成的边缘拓扑在深入 edge 实现细节之前先建立整体心智模型。React Starter Kit 将全部流量收敛到单一域名由 web worker 作为唯一的公共入口根据请求路径将流量分发到 app 与 api worker——整个过程没有任何跨 Worker 的公网 URL 往返详见 架构总览web worker唯一绑定公共主机名的 Worker承担营销站点与边缘路由器双重角色app worker纯静态资源 Worker托管 React SPATanStack Router仅通过 service binding 被 web 访问api worker运行 Hono HTTP 服务器承载 tRPC、Better Auth 与 webhooks通过 Hyperdrive 连接 Neon PostgreSQL。一条典型的请求链路是浏览器GET /settings→ web worker → service binding → app worker 返回 SPA 资源浏览器POST /api/trpc/billing.subscription→ web worker → service binding → api worker → Hyperdrive → Neon 数据库 → JSON 原路返回。这篇 edge 文档就是围绕如何配置与运维这三个 Worker展开的完整实现手册。二、Workers 配置总览每个 Worker 各有一份 wrangler.jsonc每个 Worker 都在自己的工作区目录下维护独立的wrangler.jsonc配置文档用一张表概括了三者的关键差异Worker配置nodejs_compat静态资源Service bindingswebapps/web/wrangler.jsonc否营销页面APP_SERVICE, API_SERVICEappapps/app/wrangler.jsonc否SPA bundle–apiapps/api/wrangler.jsonc是––为什么只有 api Worker 开启 nodejs_compatnodejs_compat是 Wrangler 提供的 Node.js 兼容层让依赖 Node.js 内置模块如postgres、crypto的 npm 包能在 Workers 运行时工作。仓库中仅 apps/api/wrangler.jsonc 声明了compatibility_flags: [nodejs_compat]原因很直接只有 api worker 跑真正的应用代码——它要运行postgres驱动连接数据库、使用node:util、node:fs等内置模块而 web 与 app worker 只做静态资源托管和请求代理不需要也不应该为此付出兼容层成本。这一最小权限原则贯穿整个边缘架构web worker 从不校验会话、从不查询数据库它唯一的工作是路由app worker 连自定义脚本都没有。权限面越小攻击面与故障面就越小。三、Service Bindings非继承声明与命名约定Service bindings 让 Worker 之间通过 Cloudflare 内部网络直接互相调用省去公网 HTTP 往返。web worker 声明两个绑定// apps/web/wrangler.jsonc生产环境顶层 services: [ { binding: APP_SERVICE, service: example-app }, { binding: API_SERVICE, service: example-api }, ]关键陷阱bindings 是不可继承的Wrangler 中 service bindings不可继承non-inheritable——顶层声明只对生产环境生效每个命名环境env块都必须用正确的 Worker 名称重新声明。原文档明确警告漏掉这一步会导致 staging 环境的 Worker 错误地绑定到生产服务。真实的 apps/web/wrangler.jsonc 完整展示了这一模式// apps/web/wrangler.jsonc { name: example-web, main: ./worker.ts, assets: { directory: ./dist, binding: ASSETS, run_worker_first: [/] }, // 生产顶层 services: [ { binding: APP_SERVICE, service: example-app }, { binding: API_SERVICE, service: example-api } ], env: { dev: { services: [ { binding: APP_SERVICE, service: example-app-dev }, { binding: API_SERVICE, service: example-api-dev } ] }, staging: { services: [ { binding: APP_SERVICE, service: example-app-staging }, { binding: API_SERVICE, service: example-api-staging } ] } } }Worker 命名约定project-worker-env为避免跨环境误绑仓库采用统一的命名约定生产环境省略环境后缀其余环境追加-env。Wrangler 的--env name会在部署时自动追加后缀这正好与 service bindings 的解析目标一一对应。环境WebAppAPI生产example-webexample-appexample-apiStagingexample-web-stagingexample-app-stagingexample-api-staging测试守护环境绑定不被遗忘这条非继承规则如此容易被破坏仓库专门用测试把它固化了下来。apps/app/lib/edge-routing.test.ts 中的用例会解析apps/web/wrangler.jsonc检查顶层与每个env块是否都声明了services缺失任何一个环境都会让测试失败并提示Add services to these envs in apps/web/wrangler.jsonc。这保证了任何人在 CI 阶段就会发现问题而不是等到 staging 流量被错误地打到生产服务上。四、HyperdriveWorkers 与 Neon PostgreSQL 之间的连接池Cloudflare Hyperdrive 在 Workers 与 Neon PostgreSQL 之间提供连接池与查询缓存。api worker 为每个环境声明两个绑定按缓存策略分流Binding缓存用途HYPERDRIVE_CACHED启用读多写少的查询、可容忍一定陈旧的数据HYPERDRIVE_UNCACHED禁用写入与一致性敏感的读取默认对应 apps/api/wrangler.jsonc 中的生产配置// apps/api/wrangler.jsonc hyperdrive: [ { binding: HYPERDRIVE_CACHED, id: your-hyperdrive-cached-id-here }, { binding: HYPERDRIVE_UNCACHED, id: your-hyperdrive-uncached-id-here } ]每个环境dev/staging/production都有自己独立的 Hyperdrive ID指向对应的 Neon 数据库分支——staging 环境用your-staging-hyperdrive-cached-id-here这类占位符由 Terraform apply 后的 output 回填。连接代码逐行解析Hyperdrive 绑定本身并不暴露连接池的细节它只是把connectionString交给数据库驱动。核心实现在 apps/api/lib/db.tsimport { schema } from repo/db; import { drizzle } from drizzle-orm/postgres-js; import postgres from postgres; export function createDb(db: Hyperdrive) { const client postgres(db.connectionString, { max: 1, // Two clients per request share the connection budget connect_timeout: 10, idle_timeout: 20, max_lifetime: 60 * 30, transform: { undefined: null }, onnotice: () {}, // Suppress PostgreSQL NOTICE messages }); return drizzle(client, { schema, casing: snake_case }); }四个关键设计决策每个都有明确的技术理由max: 1——单客户端单连接每个请求会构建两个客户端cached uncached而 Workers 对并发外部连接有硬性上限。每个客户端只保留 1 条连接两个加在一起也远在上限之内这是对 Workers 运行时约束的精确适配。保留 prepared statements不要关闭postgres.js 默认启用预编译语句而Hyperdrive 只会缓存它看到的预编译查询——关闭prepare: false会同时损失查询缓存和额外增加一次往返。这是文档特别强调的取舍点。origin 必须是 unpooled 主机正因为依赖预编译语句Hyperdrive 后面的 Postgres 源绝不能挂事务模式连接池transaction-mode pooler否则会破坏预编译语句机制。Neon 的连接串本身就满足这一前提。超时与生命周期参数connect_timeout: 10连接超时 10 秒、idle_timeout: 20空闲 20 秒回收、max_lifetime: 60 * 30连接最长存活 30 分钟配合transform: { undefined: null }把 JS 的undefined归一为 SQL 的NULL并用onnotice: () {}静默掉 PostgreSQL 的 NOTICE 噪音。这两个连接在 apps/api/worker.ts 的中间件中注入到请求上下文c.set(db, db)uncached默认路径与c.set(dbCached, dbCached)cached读重查询显式选用。认证Better Auth固定使用 uncached 连接因为会话数据绝不能陈旧。五、静态资源托管run_worker_first 与 SPA fallbackWeb Workerrun_worker_first强制/走 Workerweb worker 从apps/web/dist/提供营销页面其静态资源块有一个特殊设置// apps/web/wrangler.jsonc assets: { directory: ./dist, binding: ASSETS, run_worker_first: [/] }run_worker_first: [/]的含义是这些路径先执行 Worker 脚本再由 Worker 决定是否回退到静态资源。它只对/这一条路径是必需的——因为/路由需要检查认证提示 Cookie在营销页与应用仪表盘之间做选择详见下节。其余路径要么命中 Worker 的显式路由/api/*、/login*等要么直接落回静态资源无需 worker-first。App Worker纯静态 SPA fallbackapp worker 是没有自定义脚本的纯静态资源 Worker配置极其精简// apps/app/wrangler.jsonc assets: { directory: ./dist, not_found_handling: single-page-application }not_found_handling: single-page-application的含义任何未命中静态文件的路径都返回index.html由 TanStack Router 接管客户端路由。这正是 SPA 部署的标准做法——服务端只需交出 HTML 外壳页面切换全在浏览器内完成。边缘路由与 SPA fallback 的微妙冲突APP_PATHS 的路由写法里藏着一个容易踩的坑。web worker 对每个 SPA 路径注册两个精确路由而不是一个前缀通配见 apps/web/worker.tsfor (const path of APP_PATHS) { app.all(/${path}, (c) c.env.APP_SERVICE.fetch(c.req.raw)); app.all(/${path}/*, (c) c.env.APP_SERVICE.fetch(c.req.raw)); }为什么不用/${path}*因为裸前缀会误匹配共享前缀的路径——例如/members-only会被members*吞掉而 app worker 的 SPA fallback 会为它返回index.html从而把本应是营销页的/members-only变成 SPA 空壳。精确路径 斜杠后代的写法既覆盖了login、login/foo又不会误伤login-helper这类无关路径。这个约束同样被 edge-routing.test.ts 用正则断言锁死源码中不得出现裸前缀路由。六、Auth Hint Cookie 路由/的智能分流web worker 的/路由是这套边缘架构最巧妙的部分一个不拥有任何认证逻辑的边缘如何知道该给访问者看营销页还是登录后的仪表盘答案是认证提示 Cookieauth hint cookie——一个只表示是否登录过的轻量信号。路由实现核心逻辑在 apps/web/worker.tsapp.on([GET, HEAD], /, async (c) { const hasAuthHint getCookie(c, __Host-auth) 1 || getCookie(c, auth) 1; const upstream await (hasAuthHint ? c.env.APP_SERVICE : c.env.ASSETS).fetch( c.req.raw, ); // Prevent caching – response varies by auth state const headers new Headers(upstream.headers); headers.set(Cache-Control, private, no-store); headers.set(Vary, Cookie); return new Response(upstream.body, { status: upstream.status, statusText: upstream.statusText, headers, }); });要点拆解只检查值是否为1worker 从不读取会话 Cookie、从不校验会话有效性——它只关心这个 Cookie 存不存在且值为 1。认证权威始终是 app worker 内部的 Better Auth。双 Cookie 名兼容HTTPS 下用__Host-auth__Host-前缀保证 Cookie 必须带 Secure 属性、只能由同域设置本地 HTTP 开发时浏览器会拒绝__Host-前缀因此回退检查auth。这一细节记录在 ADR-001 中。缓存控制三件套Cache-Control: private, no-store与Vary: Cookie联合防止 CDN 和浏览器缓存把错误版本登录用户看到营销页或反之分发给其他人。因为响应内容随认证状态变化任何形式的共享缓存都是不安全的。路由提示 ≠ 安全边界这个 Cookie 只是路由优化手段。它的误报例如已注销但 Cookie 残留最多导致用户被多重定向一次到/login由 app worker 校验真实会话后纠正它不可能被用来伪造登录态因为真实鉴权根本不在边缘层。相关取舍记录在 ADR-001既不做边缘调用 API 校验会话耦合 延迟也不直接读 Better Auth 会话 Cookie脆弱、依赖认证库内部格式。APP_PATHS 与营销页的自动化一致性校验web worker 用硬编码的APP_PATHS_app、login、members、settings、signup决定哪些路径转发给 app worker。这份名单极易与真实的 SPA 路由、Astro 营销页脱节因此 edge-routing.test.ts 提供了四重守护从 TanStack Router 生成的routeTree.gen.ts读取全部顶层路由断言都在APP_PATHS中否则直接加载会 404从apps/web/pages目录推导营销页拥有的路径断言APP_PATHS不与其冲突否则该路由无解列入名单会遮蔽营销页不列入则直接访问 404断言路由匹配使用精确路径而非裸前缀防止 SPA fallback 吞掉共享前缀路径断言wrangler.jsonc每个环境都声明了 services防止误绑生产。其中动态路由$slug和动态营销页[slug]会被测试直接拒绝要求嵌套在静态段下——因为边缘路由只能处理字面路径。七、基础设施Terraform 提供依赖Wrangler 拥有 Worker职责边界ADR-002边缘架构的基础设施遵循一个铁律Terraform 只配置 Worker 消费的东西Wrangler 拥有 Worker 本身。两者管理字段零重叠因此永远不会出现两种工具互相拆台的漂移——这正是 ADR-002 记录的历史教训此前 Terraform 声明subdomain.enabled而每个wrangler.jsonc设workers_dev: false两个工具互相撤销对方的配置。具体分工Terraform 负责每个环境的两个 Hyperdrive 配置cached uncached以及可选的 R2 上传桶Wrangler 负责Worker 名称、代码、路由、自定义域名、bindings、vars、secrets跨界的唯一值稳定的非机密资源标识符——两个 Hyperdrive ID以及启用上传桶时的 R2 桶名。自定义域名Custom Domain也归 Wranglerweb worker 是整个主机名的 originWrangler 会创建 DNS 记录和证书因此 Terraform 完全不碰 DNS、也不需要 zone 权限见 apps/web/wrangler.jsonc 的routes块pattern 必须是裸主机名不能带/*或zone_name。目录结构与根模块infra/ ├── modules/ │ └── cloudflare/ # Hyperdrive pair, optional R2 bucket └── envs/ # One root one HCP Terraform workspace one state ├── staging/ └── production/一个目录 一个 Terraform root 一个 HCP Terraform workspace 一份 state。environment与 workspace 名称在 infra/envs/staging/main.tf 中硬编码而不是做成变量——因为可配置化会让某个 root 意外作用于另一个环境Terraform 还会在TF_WORKSPACE与名称不一致时拒绝执行。应用 Terraform 并回填 Hyperdrive ID每个环境调用一次 cloudflare 模块得到一组 cached/uncached Hyperdrive 配置module edge { source ../../modules/cloudflare account_id var.cloudflare_account_id project_slug var.project_slug environment staging # hard-coded: the directory already decided database_url var.database_url }注意database_url是未池化的 PostgreSQL 连接串与第四节中origin 必须 unpooled的约束一致并被标记为sensitive true。模块还接受origin_connection_limit默认 20即每个 Hyperdrive 配置到 origin 的软连接上限注释提醒两个配置共用该值预算时应至少翻倍再加余量。apply 之后main.tf 的 outputs 会输出hyperdrive_cached_id/hyperdrive_uncached_id粘贴到apps/api/wrangler.jsonc对应环境的hyperdrive块uploads_bucket_name未启用上传时为nullwrangler_hyperdrive_bindings一段可直接粘贴的 bindings JSON。Worker 名称则完全由wrangler.jsonc决定顶层配置部署生产--env name部署时追加-name后缀——这正是 service bindings 解析目标名称的依据。跨工具的数据流是单向的Terraform 产出 ID → 手工粘贴到 Wrangler 配置每次环境只做一次。八、本地开发模拟而非复现边缘拓扑bun dev根 package.json 的bun --filter repo/web --filter repo/api --filter repo/app dev启动三个本地开发服务器服务运行时端口说明appVite5173主要开发入口webAstro4321营销站点apiBun8787Hono 服务器app 将/api/*代理到此本地的服务绑定拓扑并不复现线上形态——没有三个 Worker 互连只有两个代理关系Vite 代理app 的 Vite 开发服务器把/api/*代理到:8787的 Bun 服务器getPlatformProxy 模拟 Hyperdriveapps/api/dev.ts 用 Wrangler 的getPlatformProxy()environment: devpersist: true在.wrangler目录保留跨重启状态模拟两个 Hyperdrive 绑定。persist会维护状态例如模拟环境的 KV/Durable Object 数据在重启后不丢失。本地环境变量不是 DATABASE_URL这是文档特别澄清的一点本地 Hyperdrive 绑定解析自CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_*变量而不是DATABASE_URL。DATABASE_URL属于db/目录下的 Drizzle 工具链用于db:migrate、db:push等与运行时数据通道完全无关。命名遵循绑定名约定CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE_CACHEDCLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE_UNCACHED由于本地直连 Postgres本地没有连接池也没有查询缓存——Hyperdrive 的池化与缓存特性只在 Cloudflare 边缘生效。本地 env 合并逻辑dev.ts 的中间件展示了本地环境变量的完整合并顺序先取getPlatformProxy暴露的 Cloudflare 绑定含两个 Hyperdrive再用本地process.env覆盖 envSchema 中其余字段自动从 schema 推导 key新增字段无需改代码APP_ORIGIN则按x-forwarded-origin请求头 →process.env.APP_ORIGIN→ 绑定值 → 默认http://localhost:5173的优先级解析。ENVIRONMENT与APP_ORIGIN两个字段被排除在通用覆盖之外由专门逻辑处理。createAuth(db, env)固定使用 uncached 连接与生产 worker.ts 的中间件行为保持一致。别忘了先构建邮件模板API 服务器导入的是编译后的 email 包因此启动 API 开发服务器之前必须完成邮件模板构建。bun dev脚本已自动处理这一依赖——它先运行bun email:build再启动三个服务。若手动启动单个 API 服务如bun api:dev请先执行bun email:build或根目录的bun run buildBun 会按 email → API 的依赖顺序编排同时并行执行相互独立的 web/app 构建。九、关键不变量设计边界的最终校验清单综合 边缘架构文档 与 架构总览这套边缘拓扑的六条核心不变量是排查问题时的第一思维框架api worker 是认证与数据访问的唯一权威——web worker 永不校验会话、永不查询数据库只有 web worker 暴露公共路由——app 与 api 仅通过 service bindings 可达service bindings 不可继承——每个 Wrangler 环境必须声明自己的 bindings认证提示 Cookie 是路由优化而非安全机制——误报只造成一次额外重定向api worker 是唯一启用nodejs_compat的 WorkerWrangler 与 Terraform 字段零重叠——哪个工具拥有这个字段永远只有一个答案。这六条不变量决定了从wrangler.jsonc的每一行配置到worker.ts的每一条路由的写法也定义了问题排查时的边界路由问题查 apps/web/worker.ts数据问题查 apps/api/lib/db.ts 与 Hyperdrive环境问题查各wrangler.jsonc的env块基础设施问题查 infra/envs 与 ADR-002。理解了这份 edge 实现手册你就掌握了整个 React Starter Kit 从浏览器请求到数据库响应的完整链路。【免费下载链接】react-starter-kitModern React starter kit with Bun, TypeScript, Tailwind CSS, tRPC, Stripe, and Cloudflare Workers. Production-ready monorepo for building fast web apps.项目地址: https://gitcode.com/gh_mirrors/rea/react-starter-kit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考