- 后端
- 前端
【免费下载链接】react-starter-kit
Modern React starter kit with Bun, TypeScript, Tailwind CSS, tRPC, Stripe, and Cloudflare Workers. Production-ready monorepo for building fast web apps.
本指南以 docs/database/index.md 为核心,系统讲解 react-starter-kit 中db/workspace 的数据层设计:如何用 Drizzle ORM 定义 schema、通过 Cloudflare Hyperdrive 在边缘连接 Neon PostgreSQL、如何在本地开发中模拟生产绑定,以及一整套面向多环境(dev/staging/production)的迁移、播种与备份命令体系。读完你将掌握这套 monorepo 数据库层的完整架构、每一条bun db:*命令的真实行为与适用场景,以及"环境定位(Environment Targeting)"这套防止误操作生产库的安全设计。
数据层定位:db/workspace 的技术选型
react-starter-kit 的数据层是一个独立的db/workspace,组合了三项关键技术:
- Drizzle ORM:TypeScript 优先的轻量 ORM,schema 即代码,迁移文件由 Drizzle Kit 自动生成;
- Neon PostgreSQL:托管 Postgres 服务,作为生产数据源;
- Cloudflare Hyperdrive:在边缘(Edge)对数据库连接做池化(pooling)与可选查询缓存(caching),让 Cloudflare Workers 上的 API 以更低的连接开销访问 Postgres。
从仓库根目录看,数据层相关的 workspace 声明位于根 package.json 的workspaces中("db"被显式列出),所有数据库命令通过根脚本转发到db/内部执行(见下文「命令速查表」)。
Workspace 目录结构
原文档给出的db/结构如下,与仓库实际内容一致:
db/ ├── schema/ # Table definitions and relations ├── migrations/ # Auto-generated SQL migrations ├── seeds/ # Seed data scripts ├── scripts/ # Utilities (seed runner, export) ├── drizzle.config.ts # Drizzle Kit configuration └── index.ts # Re-exports schema + DatabaseSchema type各目录的实际职责(结合源码确认):
schema/:按领域划分的表定义与关系文件。从 db/schema/index.ts 看,它统一 re-export 了id、invitation、organization、passkey、subscription、user六个模块;migrations/:Drizzle Kit 自动生成的迁移 SQL(含meta/下的快照与 journal 文件),见 db/migrations;seeds/:种子数据脚本,如 db/seeds/users.ts;scripts/:工具脚本,包括 db/scripts/seed.ts(播种入口)与 db/scripts/export.ts(pg_dump 备份);drizzle.config.ts:Drizzle Kit 配置,下文详述;index.ts:对外导出 schema 与DatabaseSchema类型。
一个文件对应一组实体
原文档指出:schema 文件按实体组组织,每个文件负责一组实体。例如 db/schema/user.ts 内同时定义了与 Better Auth 兼容的user、session、identity(OAuth/密码凭据,即 Better Auth 中的account表)、verification(邮箱验证、密码重置等令牌)四张表,并通过relations()声明了外键关系与索引。所有表再经由schema/index.ts统一对外导出。
连接架构:Hyperdrive 双绑定与 tRPC 上下文
生产环境中,API worker 通过 Cloudflare Hyperdrive 连接 Neon,Hyperdrive 在边缘提供连接池和可选的查询缓存。仓库为 API worker 配置了两个 Hyperdrive 绑定,原文档给出了明确的用途划分:
| Binding | Cache | Use for |
|---|---|---|
HYPERDRIVE_CACHED | 60 s + 15 s stale by default | Read-heavy queries where staleness is acceptable |
HYPERDRIVE_UNCACHED | None | Writes and anything requiring fresh data |
(缓存窗口的默认值 "60 s + 15 s stale" 由 Terraform 侧的 Hyperdrive 配置决定,参见 infra/envs/production/main.tf 与 infra/envs/staging/main.tf。)
两个绑定在 apps/api/wrangler.jsonc 中声明,并且 dev、staging、production 三个环境各自重复声明(生产环境需要把id替换为真实创建的 Hyperdrive ID):
"hyperdrive": [ { "binding": "HYPERDRIVE_CACHED", "id": "your-hyperdrive-cached-id-here" }, { "binding": "HYPERDRIVE_UNCACHED", "id": "your-hyperdrive-uncached-id-here" } ]在 tRPC 上下文中暴露ctx.db与ctx.dbCached
两个绑定在 tRPC context 中分别暴露为ctx.db(uncached)与ctx.dbCached(cached)。apps/api/lib/context.ts 的类型注释把这条规则写进了代码:
db:始终新鲜的读取,是默认选择;dbCached:读取命中 Hyperdrive 的缓存窗口且写入后不会失效,只在可接受陈旧数据的地方启用——绝不能用于 auth、权限、计费状态,或"先写后读"场景。
原文档特别强调:Better Auth 使用db(uncached),因为过期的 session 或角色行会"活过"登出或权限变更。这与context.ts中"never for auth, permissions, billing state"的注释相互印证。
createDb源码剖析
原文档给出了简化版createDb,仓库中的完整实现位于 apps/api/lib/db.ts:
export function createDb(db: Hyperdrive) { const client = postgres(db.connectionString, { // Each request builds two clients (cached + uncached), and Workers caps // concurrent external connections. One apiece stays well inside that. max: 1, connect_timeout: 10, // Prepared statements left on (postgres.js default). Hyperdrive only caches // queries it sees prepared; `prepare: false` would cost it the cache and an // extra round-trip. This is why the origin must be an unpooled host – a // transaction-mode pooler in front of Postgres breaks prepared statements. idle_timeout: 20, max_lifetime: 60 * 30, transform: { undefined: null, }, onnotice: () => {}, }); return drizzle(client, { schema, casing: "snake_case" }); }几个值得注意的底层细节:
max: 1:每个请求会构建两个客户端(cached + uncached),而 Workers 对并发外部连接数有限制,每个绑定各 1 条连接即可稳妥地控制在预算内;- prepared statements 保持开启(postgres.js 默认):Hyperdrive 只缓存它识别为 prepared 的查询,若设置
prepare: false将失去缓存并多一次往返。这解释了为什么 Hyperdrive 的源必须是非池化的主机——事务模式连接池(transaction-mode pooler)位于 Postgres 之前会破坏 prepared statements; transform: { undefined: null }:将undefined统一转换为NULL写入数据库;- 返回的 Drizzle 实例使用
schema(全量 schema)并开启casing: "snake_case",与drizzle.config.ts中的命名约定保持一致。
Env 类型中为何没有DATABASE_URL
值得顺带一提:API worker 的运行时环境契约 apps/api/lib/env.ts 中不包含DATABASE_URL。注释明确写道:数据库通过 Hyperdrive bindings 到达,而不是连接字符串;DATABASE_URL只属于db/下的 drizzle-kit 进程(用于生成迁移、连接 Studio 等本地工具)。
本地开发:getPlatformProxy()模拟 Hyperdrive
原文档用一个::: info块专门说明了本地开发的关键差异,这是最容易踩坑的地方:
- 开发时,Wrangler 的
getPlatformProxy()会在本地模拟 Hyperdrive bindings; - 每个绑定从各自的
CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE_*变量(位于.env)解析连接串,而不是DATABASE_URL; DATABASE_URL只被db/中的 Drizzle 工具读取,两者井水不犯河水;- 本地绑定直连 Postgres,因此连接池与查询缓存都不生效;
- 代码在两种环境下使用完全相同的
HYPERDRIVE_CACHED/HYPERDRIVE_UNCACHED绑定名称——无需任何条件连接逻辑。
这与 apps/api/wrangler.jsonc 中dev环境的占位符 ID("local-dev-placeholder")相呼应:本地不会真正解析这些 ID,绑定连接串来自本地环境变量。
命令速查表
所有命令从仓库根目录运行。原文档给出的速查表如下(部分命令带:staging或:production后缀以切换目标环境):
| Command | Description |
|---|---|
bun db:generate | Generate migration SQL from schema changes |
bun db:migrate | Apply pending migrations |
bun db:push | Push schema directly (skips migration files) |
bun db:studio | Open Drizzle Studio browser UI |
bun db:seed | Run seed scripts |
bun db:check | Check generated migration history for conflicts |
bun db:export | Export database via pg_dump todb/backups/ |
bun db:typecheck | Run TypeScript type-checking on thedb/workspace |
命令的真实映射
根 package.json 把每条命令转发到db/workspace,而 db/package.json 定义了实际执行内容:
| 根命令 | db 内脚本 | 实际执行 |
|---|---|---|
bun db:generate | generate | bun --bun drizzle-kit generate |
bun db:migrate | migrate | bun --bun drizzle-kit migrate |
bun db:migrate:staging | migrate:staging | ENVIRONMENT=staging bun --bun drizzle-kit migrate |
bun db:migrate:production | migrate:production | ENVIRONMENT=production bun --bun drizzle-kit migrate |
bun db:push | push | bun --bun drizzle-kit push |
bun db:studio | studio | bun --bun drizzle-kit studio |
bun db:seed | seed | bun scripts/seed.ts |
bun db:seed:staging | seed:staging | ENVIRONMENT=staging bun scripts/seed.ts |
bun db:export | export | bun scripts/export.ts |
bun db:export:production | export:production | ENVIRONMENT=production bun scripts/export.ts |
bun db:check | check | bun --bun drizzle-kit check |
bun db:typecheck | typecheck | tsc --noEmit |
此外db/package.json还保留了introspect、up、drop等 Drizzle Kit 脚本,但未暴露到根命令,属于按需直接调用的工具。
两个工具脚本的行为细节
db:seed(db/scripts/seed.ts):通过import "../drizzle.config"触发环境加载与校验,然后以postgres(DATABASE_URL, { max: 1 })建连、drizzle(client, { schema, casing: "snake_case" })实例化,调用 db/seeds/users.ts 的seedUsers(db)插入 10 个测试用户(如alice@example.com、bob@example.com),使用onConflictDoNothing()保证幂等,成功后输出 ✅ 并关闭连接。
db:export(db/scripts/export.ts):是一个功能完整的 pg_dump 包装脚本,支持:
- 默认只导出 schema(
--schema-only); --data导出 schema + 数据(-full);--data-only仅导出数据(-data);--table=<name>只导出指定表;--之后的所有参数原样透传给 pg_dump(例如bun db:export -- --inserts)。
它要求系统装有pg_dump(运行时通过which pg_dump校验),输出文件命名含时间戳、环境后缀与类型后缀(如dump-staging-full-2026-09-19T05-00-00.sql),自动创建db/backups/目录,并把输出文件权限收紧为0600(仅属主可读),失败时以退出码 1 结束以便 CI/CD 集成。
环境定位:ENVIRONMENT变量与 fail-closed 设计
数据库脚本通过ENVIRONMENT变量选择环境,未设置时回退到NODE_ENV。这套逻辑完整实现在 db/drizzle.config.ts 中:
- 显式传入
ENVIRONMENT时,development会归一化为dev,其余必须是dev/test/staging/production之一,否则直接抛错——防止拼写错误静默落到开发库; - 未显式传入时,按
NODE_ENV映射:production→ production,staging→ staging,test→ test,其余一律视为dev。
开发环境:级联加载,先到先得
开发环境按first value wins规则级联加载环境文件:
.env.dev.local → .env.local → .env实现上,db/drizzle.config.ts 依序对这三个文件执行configDotenv(默认不覆盖),因此shell 中已导出的变量优先级最高,与 Vite 的约定一致。
staging / production:fail-closed,绝不串库
staging 与 production不做级联,这是整套设计中最关键的安全保证:
bun db:migrate:production只读取.env.production.local这一个文件,且override: true,文件中的值会覆盖任何已导出的DATABASE_URL;- 若该文件缺失,命令直接失败而不是继续回退——db/drizzle.config.ts 会抛出
Missing .env.production.local – refusing to target production; - 若文件中没有定义
DATABASE_URL,同样拒绝执行(refusing to reuse a value inherited from the shell or another tool)。
由此保证:任何带环境后缀的命令永远不可能落到另一个环境的数据库上。
哪些命令有远程变体,哪些没有——以及为什么
原文档给出了设计意图明确的对照表:
| Command | Remote variants | Why |
|---|---|---|
db:migrate,db:studio,db:export | Yes | Applying migrations, inspecting and backing up are real remote operations |
db:seed | :stagingonly | Seeds create test accounts – they have no business in production |
db:generate | No | Reads the schema and existing migrations; it never connects to a database |
db:push | No | Syncs schema without a migration file – prototyping only, never deployed |
- 迁移、Studio 巡检、导出是对远端数据库的真实操作,因此都有 staging/production 变体;
- seed 只提供
:staging:种子数据创建的是测试账号,不该出现在生产库;从源码看 db/scripts/seed.ts 注释也明确"put real reference data in a migration instead"(真实参考数据应放进迁移而非种子); db:generate只读 schema 与已有迁移,从不连库,所以不需要环境变体;db:push跳过迁移文件直接同步 schema,仅限原型阶段,永远不会用于部署,因此同样没有远程变体。
DATABASE_URL的格式约束
DATABASE_URL必须是合法的postgres://或postgresql://连接串。db/drizzle.config.ts 在启动时用正则/^postgre(s|sql):\/\/.+/校验,不合法直接抛错DATABASE_URL must be a valid PostgreSQL connection string。
完整的变量清单与说明见 docs/getting-started/environment-variables.md。值得再次强调其边界:DATABASE_URL只服务于db/的本地工具链;运行时 API 通过 Hyperdrive 绑定取连接串(见 apps/api/lib/env.ts 的注释)。
导入 Schema:@repo/db的双入口
db/workspace 对外以@repo/db包提供两个入口(见 db/package.json 的exports):
import * as schema from "@repo/db"; // full schema + DatabaseSchema type import { user, session } from "@repo/db/schema"; // individual tables@repo/db(即 db/index.ts)导出完整 schema、schema命名空间对象以及DatabaseSchema = typeof schema类型;@repo/db/schema指向 db/schema/index.ts,可单独按需引入单张表。
API worker 侧的消费方式可从 apps/api/lib/db.ts 与 apps/api/lib/context.ts 看到:import { schema } from "@repo/db"用于构建 Drizzle 实例,import type { DatabaseSchema }用于声明PostgresJsDatabase<DatabaseSchema>类型。
源码级延伸:schema 组织与 ID 设计
虽然本指南以docs/database/index.md为主体,但结合仓库源码可以更完整地理解其"按领域组织 schema"的含义。
按领域分组的表
db/schema 下的六个文件对应六组实体:
user.ts:Better Auth 兼容的四张核心表(user/session/identity/verification),并定义了userRelations、sessionRelations、identityRelations及session_user_id_idx等索引;organization.ts:组织与成员;invitation.ts:组织邀请;passkey.ts:WebAuthn 通行密钥;subscription.ts:订阅(与 Stripe 计费联动,见 docs/billing/index.md);id.ts:ID 生成工具。
前缀化 CUID2 ID
db/schema/id.ts 为所有实体生成带前缀的 CUID2 主键,格式为{prefix}_{body},共 20 字符,例如usr_ght4k2jxm7pqbv01:
- Better Auth 模型前缀:
usr(user)、ses(session)、idn(identity,对应表名identity,避免与计费"account"混淆)、vfy(verification)、org、mem、inv、pky; - 非 auth 表通过
generateId(prefix)生成,前缀必须恰好是 3 个小写字母; - 设计理由详见 docs/specs/prefixed-ids.md。
进一步阅读
围绕数据层的其余专题文档均位于仓库docs/database/目录下,可继续深入:
- docs/database/schema.md:表结构细节与字段说明;
- docs/database/migrations.md:迁移文件管理与工作流;
- docs/database/queries.md:查询编写与
ctx.db/ctx.dbCached使用规范; - docs/database/seeding.md:种子数据脚本编写;
- docs/api/context.md:tRPC 上下文(
ctx.db的消费方)详解; - docs/architecture/edge.md:边缘架构中 Hyperdrive 的定位。
- 后端
- 前端
【免费下载链接】react-starter-kit
Modern React starter kit with Bun, TypeScript, Tailwind CSS, tRPC, Stripe, and Cloudflare Workers. Production-ready monorepo for building fast web apps.
相关推荐
Xwayland Satellite高级配置:监听文件描述符和按需激活机制
Xwayland Satellite高级配置:监听文件描述符和按需激活机制 Xwayland Satellite是一款实现Xwayland outside yo
react-starter-kit 生产数据库部署指南:Neon PostgreSQL 与 Cloudflare Hyperdrive 的端到端配置
react starter kit 生产数据库部署指南:Neon PostgreSQL 与 Cloudflare Hyperdrive 的端到端配置 本文是 r
后端前端React Starter Kit 数据库 Schema 设计指南:基于 Drizzle ORM 与 Better Auth 的 PostgreSQL 表结构实战
React Starter Kit 数据库 Schema 设计指南:基于 Drizzle ORM 与 Better Auth 的 PostgreSQL 表结构实
后端前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考