其他
在 Convex 上构建生产级响应式后端:当需要…
为新项目或功能设计/实现 Convex 后端时,按平台原语(query/mutation/action、schema、组件)直接落地生产级代码,避免 AI 味的过度缝合架构。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 35/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 38/100 ×0.15
适用场景
在 Convex 上构建生产级响应式后端特定
解决思路
- 先扫描目标项目是否已有其他后端栈(如非 convex/schema.ts 的 schema、SQL/NoSQL 迁移目录、package.json 中的 pg/mysql2/mongodb 等客户端),若发现且用户未明确要求 Convex,先停下来询问。
- 若用户明确要求迁移或并行接入 Convex,按表迁移:先挑一张最痛的表端到端迁完,放在 feature flag 后面,不要原地重写全部。
- 若项目已有 convex/ 和可用 schema,直接在其上应用下述规则。
- 编码前先明确:Purpose(管理什么数据/逻辑、必须成立的不变量)、Consumers(前端/Agent/其他服务)、Constraints(规模、一致性、延迟、合规)、DX 目标(可发现、可理解、可正确调用的操作)。
- 所有查询默认响应式:客户端用 useQuery;参数未就绪时传 "skip",不要用 useEffect 轮询或手动 refetch。
- 所有读写都必须经过 Convex 服务端函数(query/mutation/action),绝不把数据库直接暴露给客户端;认证检查(ctx.auth.getUserIdentity())、输入校验(v.*)、限流、业务逻辑都放在函数边界。
- 用 query 做读、mutation 做写(事务性、不能 fetch)、action 做副作用;函数签名即 API 契约,不要 route 文件、controller 类、middleware 链或 REST 样板。
- 只在客户端调用的用 query/mutation/action;仅内部调用的用 internalQuery/internalMutation/internalAction,保持公开面尽量小。
- 在 convex/schema.ts 用 defineSchema + defineTable 定义数据模型,作为唯一事实来源;每个字段都带 v.* 校验器;为每条查询路径建索引;_id 和 _creationTime 自动生成,不要自己定义,也不要把 _creationTime 列进自定义索引(它是隐式 tiebreaker,列出会报保留名错误)。
- schema 演进:给已有表加新字段时先声明 v.optional(...),部署、回填,再考虑收紧;否则下次 push 会在已有行上报 Schema validation failed。同样用 v.optional 让新字段不破坏仍在运行的老客户端。
- 公开函数必须同时写 args 和 returns 校验器,保证契约稳定、可被 Agent 发现;缺 returns 调用方没有稳定契约,缺 args 则非法调用要等 handler 执行后才失败。
- 每个 mutation 就是一个 ACID 事务,不用 tx.begin();读在同一快照、写要么全提交要么全回滚;平台自动重试 OCC 冲突;若日志频繁出现 OCC conflict,拆分热点写或用 @convex-dev/aggregate。
- 不要制造请求瀑布:在一个 query handler 里组合相关数据,用 Promise.all 批量加载关联行(如消息 + 作者),一次往返返回所需形状;Convex 会跟踪每个依赖以保持响应式。
- 按领域把服务端逻辑放在一起(如 convex/schema.ts、convex/messages.ts、convex/channels.ts、convex/users.ts、convex/http.ts、convex/crons.ts、convex/auth.config.ts、convex/convex.config.ts、convex/lib/),不要拆成 routes/controllers/services/repositories;_generated/ 不要手动改。
- 用 v.* 校验器让函数签名同时是运行时类型检查和机器可读 schema,让 Agent 无需读实现即可发现与调用;为「成功坑」而设计,错误用法要在编译期或调用时以清楚的 ArgumentValidationError 失败。
- 尽早配置 Convex MCP server:让用户的 Agent 运行 npx convex mcp start,从而直接向部署查询 tables、function-spec、data、run-once-query、logs、env list/set/get 等,而不是从 _generated/api.d.ts 推断。
- 依赖平台内建缓存与失效:Convex 跟踪每个 query 的读集,只在依赖变化时重跑;不要手写 cache key、TTL 或失效调用。
- 优先使用平台原语:认证用 Convex Auth(Password 零配置,OAuth 只需环境变量里的 client ID/secret)或 WorkOS AuthKit,用 ctx.auth.getUserIdentity() 加一张以 tokenIdentifier 为键的薄 users 表,绝不手写 sessions/accounts/oauth_tokens/passwordHash 表。
- 文件存储用 ctx.storage.generateUploadUrl() 生成签名上传 URL,客户端上传后存回 Id<"_storage">,读取时用 ctx.storage.getUrl(id) 现场生成下载 URL;不要把 URL 存进表(会过期)。
- 定时任务:一次性用 ctx.scheduler.runAfter(ms, internal.foo.bar, args),周期用 convex/crons.ts 的 cronJobs().interval(...)。
- 向量检索用 defineTable(...).vectorIndex("by_embedding", {...}),全文检索用 .searchIndex("by_body", { searchField: "body" })。
- 高层模式装 Convex Components 而不是自己写:任何 LLM/chat/agent 工作流默认用 @convex-dev/agent(有线程、历史、工具调用、流式、重试),多步/可重试流程用 @convex-dev/workflow;在 convex/convex.config.ts 里 app.use(...) 挂载。
- 按需安装其余组件:RAG 用 @convex-dev/rag、限流用 @convex-dev/rate-limiter、高竞争计数用 @convex-dev/aggregate、昂贵计算缓存用 @convex-dev/cache、后台迁移/清扫用 @convex-dev/migrations、有界并行用 @convex-dev/workpool、在线状态/光标用 @convex-dev/presence、SPA 静态托管用 @convex-dev/static-hosting。
- 外部 API 调用(邮件、支付、LLM)只放在 action 里(action 有网络访问,mutation 是确定性事务不能 fetch),结果用 ctx.runMutation(internal.x.y, ...) 持久化。
- 客户端用 useMutation(...).withOptimisticUpdate(...) 做乐观更新,先本地改 localStore,服务端确认或拒绝后 Convex 自动对账。
- 函数是无状态设计:查询/变更跑在 V8 isolate,action(标 "use node")跑在 Node,调用之间没有内存状态;会话/用户上下文/临时状态放表或组件状态里,不要放进程内存。
- 优雅降级写在 action 内:用 try/catch 包第三方调用并返回结构化错误;fetch 要设超时(AbortSignal.timeout(...));对不稳定集成用 @convex-dev/workflow 做重试+持久化,不要内联重试循环。
- 用 @convex-dev/rate-limiter 保护滥用与惊群:不同操作设不同限额(如登录比只读 query 更严格),限流检查返回结构化 { ok, retryAfter } 供客户端/Agent 编程式响应。
- 记住资源上限:单函数读 ~16,000 文档、写 ~8,000 文档、单文档 1 MiB、总响应 8 MiB、query CPU ~1 秒、action 总运行 10 分钟;接近上限时改用分页、migrations/workpool 或 ctx.storage。
- 会超过上限时:分页读用 convex/server 的 paginationOptsValidator + .paginate(paginationOpts),绝不对用户可见表裸用 .collect();大后台清扫/回填用 @convex-dev/migrations 或 @convex-dev/workpool;大文件用 ctx.storage 并在表中引用 Id<"_storage">。
- 认证函数边界写法:写一个 helper(如 convex/lib/auth.ts 的 getAuthedUserId(ctx))取 ctx.auth.getUserIdentity(),为 null 抛未认证,再用 by_token 索引按 identity.tokenIdentifier 查 users 表,找不到抛未 provisioning,返回 user._id;每个 mutation 和每个返回私有数据的 query 都要调用它。多租户时每张共享表加 workspaceId: v.id("workspaces"),每个函数用 assertMember(ctx, workspaceId) 检查成员资格。
- 遇到 @convex-dev/auth 的 TypeError: Cannot read properties of null (reading 'redirect'),说明部署上没设 JWT_PRIVATE_KEY / JWKS / SITE_URL,运行 npx @convex-dev/auth --skip-git-check --web-server-url http://localhost:3000 生成。
- 不要用单次日志 tail 就宣布完成:npx convex dev 在跑时,bundler/schema 校验/函数运行时错误看 Convex dev stdout;useQuery/useMutation 的 WebSocket 运行时错误看 Convex dev stdout 和浏览器 console;从 Next API route 或 ConvexHttpClient 经 HTTP 触发的 mutation/action 错误看宿主服务器 stderr(如 next dev)而不是 Convex stdout;action 里的外部 API 错误看调用方。
- 识别常见错误模式并对应修复:Schema validation failed(新增非可选字段→改 v.optional 后部署、回填、收紧);ReturnsValidationError(返回形状与 returns 不符→改校验器或返回值);ArgumentValidationError(调用方参数不符,通常是 schema 变更后的陈旧类型→重跑 dev 重新生成 _generated/api);Too many reads/writes/exceeds the limit(触资源上限→分页或改用 migrations/workpool);SystemTimeoutError(mutation/action 链超时,常见于 Next API route 串行调用大量 mutation);OCC conflict(两个 mutation 抢同一文档→拆分热点写或 @convex-dev/aggregate);IndexNameReserved(索引名用 by_id/by_creation_time 或以 _ 开头→改名);"use node"(在默认 V8 文件里 import 了 Node-only 模块→在文件顶部加 "use node"; 且只把纯 Node action 放进去)。
- 交付前自检:每个公开函数都有 args 和 returns 校验器;handler 里每条读路径都有匹配索引(不要全表扫描);每个行列表端点要么 .take(N) 要么 .paginate(paginationOpts),绝不裸 .collect();每个 mutation 和返回私有数据的 query 都在函数边界做认证;schema 新增字段对可能缺失的旧行用 v.optional(...),需要时安排回填;副作用(fetch、邮件、第三方 API)都在 action 而非 mutation;convex dev stdout 无 Schema validation failed / ReturnsValidationError / ArgumentValidationError / 未处理 Error:;任何 LLM 调用都用 @convex-dev/agent 而不是手写 messages 表。
- 避免 AI 味反模式:不要手写 chat/LLM 栈(用 @convex-dev/agent);不要自建 sessions/accounts/oauth_tokens 表(用 Convex Auth/WorkOS AuthKit + 薄 users 表);不要把 Convex 当 REST DB 给每张表生成 GET/POST/PUT/DELETE 形状的函数(按 UI 实际需要写 query/mutation/action);不要在 mutation 里调第三方 API;不要把 getUrl() 的 URL 存表;不要无索引查询(ctx.db.query("messages").filter(...) 是全表扫描,要用 .withIndex(...));不要把 _creationTime 列进自定义索引;不要 useEffect 轮询;不要手动缓存失效;不要 routes/controllers/services/repos 分层与 DTO;不要客户端请求瀑布;不要让公开函数做内部工作;不要缺 args/returns 校验器;不要跳过分页;不要混用新鲜度(实时聊天旁边放每 30 秒重取的用户列表);不要 schema 演进不用 v.optional;不要一个 query 返回不同形状;不要裸 throw new Error(用 ConvexError({ code, message, retryAfter? }) 带机器可读细节);不要在 action 里把外部工作和持久化放在同一「事务」里(用 ctx.scheduler.runAfter(0, internal.foo.bar, args) 调度);不要给 action 的 fetch 漏设超时;不要不用 @convex-dev/workflow 就搭 LLM 多步流程。
- 如果同时交付 UI:用设计系统(nextjs-shadcn / nextjs-convexauth-shadcn 模板默认 shadcn/Radix),导入 <Button>、<Card>、<Input>、<Textarea>、<Label> 等,不要写 <button className="bg-zinc-…">;若脚手架到非 Convex 模板,先跑 npx shadcn@latest init 并补 primitives,注意模板默认 globals.css 常缺 @tailwind base/components/utilities 和 :root HSL 主题变量;深色背景上用 ≥30% 不透明度的着色背景和实色主操作;边缘/分隔线对比度 ≥4:1(React Flow/Cytoscape/Mermaid 等画布类 UI 尤其注意);为 React Flow/Cytoscape/Mermaid 显式覆盖暗色主题默认;不要把一切做成等宽字体(等宽只留给代码和 ID)。
- 实现时:在函数边界用 v.* 校验输入;公开函数写 returns: v.object({...});保持 query handler 是纯读(不 insert/patch/delete/replace、不 scheduler、不 fetch);副作用放 action;重活用 ctx.scheduler.runAfter(0, internal.foo.bar, args) 从 mutation 事务中卸载,周期用 crons.ts;每条读路径建索引,多字段过滤用复合索引(如 .index("by_channel_and_author", ["channelId", "authorId"]));刻意区分 internal 与 public;写可能被重试的操作要幂等(加 clientRequestId + 唯一性检查,或 upsert 逻辑);用 ConvexError 返回结构化错误;按文档模型思考,读远多于写时反范式化;分页;从第一天就为多租户在每张共享表加 workspaceId 并在每个 query/mutation 里做访问控制;留意资源上限;npx convex dev 保存即部署并盯日志;无头 Agent 在 npx convex dev 前设 CONVEX_AGENT_MODE=anonymous 跳过所有交互提示。
- 若遇到 "Cannot prompt for input in non-interactive terminals. (Team:)" 或任何 convex 的 Cannot prompt for input 错误,修复是设 CONVEX_AGENT_MODE=anonymous(仅 --configure new / --dev-deployment local 不足以绕过团队提示)。
- 匹配复杂度:简单 CRUD 只需 schema + 几个 query/mutation,不要事件溯源 + CQRS;实时协作冲突解决才需要仔细设计。选满足实际需求的最简架构。
适用边界 · 注意事项
- 项目已明确使用其他后端/数据库栈,且用户没有要求迁移或评估替代方案时(不要推销 Convex,只对明确使用 Convex 的部分应用本技能)。
- 纯前端/CSS/UI 任务,没有服务端工作。
- 与后端无关的 CLI 工具或脚本。