name: convex-reactive-backend title: Convex 响应式后端与实时订阅 description: 当用 Convex 搭建/接入响应式后端,需设计 Schema、编写 TS 函数(query/mutation/action)、做实时订阅、鉴权、文件存储或定时任务时使用;产出可运行的 convex/ 函数、Schema、索引与客户端订阅代码;不适用于传统 SQL/ORM 或非 Convex 的 BaaS。触发词:Convex、响应式后端、实时订阅、useQuery domain: 研发/backend triggers: [Convex, convex dev, 响应式后端, 实时订阅, useQuery, useMutation, Convex Schema, Convex 鉴权, convex action, 定时函数 cron] tags: [convex, 响应式后端, baas, typescript, 实时订阅, schema设计, 鉴权, 文件存储, 定时任务, next.js] level: 进阶 status: stable agents: [claude-code, codex, cursor, gemini-cli] tools: [Convex CLI (npx convex), TypeScript, React/Next.js, convex/react, @convex-dev/auth] requires: [] related: [] combines_with: [] license: MIT source: sickn33/antigravity-awesome-skills source_license: MIT
何时使用
- 用 Convex 作为后端新建项目,或把 Convex 接入既有 React / Next.js / Angular / Vue / Svelte / React Native 应用。
- 设计 Convex「文档-关系型」数据库 Schema,编写或调试 query / mutation / action。
- 实现实时/响应式数据:query 在底层数据变化时自动重跑并推送到所有连接的客户端。
- 配置鉴权(Convex Auth 原生库,或 Clerk / Auth0 / Better Auth 等第三方),使用文件存储、定时函数(scheduler)与 cron。
不该用边界:
- 需要原生 SQL、复杂 JOIN 或自托管传统数据库 → 用 ORM/SQL 方案,本技能不覆盖。
- 只想对比选型而非落地 Convex(如纯 Firebase / Supabase / Prisma 场景)→ 见互见,不在此重写。
- 单文档超过 1MB 的大二进制 → 必须走文件存储,不要塞进文档。
步骤
- 初始化:
npm install convex后运行npx convex dev。它会引导 GitHub 登录、创建项目与部署、生成convex/目录、实时同步函数,并写入.env.local(CONVEX_DEPLOYMENT、NEXT_PUBLIC_CONVEX_URL)。convex/_generated/为自动生成,禁止手改。 - 定 Schema:在
convex/schema.ts用defineSchema/defineTable和验证器v描述表、字段与索引(普通索引.index、全文.searchIndex、向量.vectorIndex)。 - 写函数:query 读、mutation 写(ACID 事务、可串行化隔离)、action 调外部 API。给每个函数加
args验证器。 - 接客户端:用
ConvexProvider包裹应用,组件内用useQuery/useMutation/usePaginatedQuery订阅与写入。 - 进阶:按需加鉴权、文件存储、
ctx.scheduler定时任务与crons.ts。 - 部署:
npx convex deploy;用npx convex env set配置环境变量。
指令
npx convex dev # 开发:监听改动并同步到 dev 部署
npx convex deploy # 部署到生产
npx convex run tasks:list # CLI 直接调函数
npx convex import --table tasks data.jsonl
npx convex export --path ./backup
npx convex logs / dashboard
npx convex env set OPENAI_API_KEY sk-... # 仅 action 内可读 process.env
npx convex env list / unset OPENAI_API_KEY
函数能力对照(关键约束):
| 类型 | 用途 | 读库 | 写库 | 调外部 API | 缓存/响应式 |
|---|---|---|---|---|---|
| Query | 读数据 | ✅ | ❌ | ❌ | ✅ |
| Mutation | 写数据(ACID) | ✅ | ✅ | ❌ | ❌ |
| Action | 副作用 | 经 runQuery |
经 runMutation |
✅ | ❌ |
| HTTP Action | Webhook/端点 | 经 runQuery |
经 runMutation |
✅ | ❌ |
示例
Schema 与索引(convex/schema.ts):
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
messages: defineTable({
authorId: v.id("users"),
channelId: v.id("channels"),
body: v.string(),
attachmentId: v.optional(v.id("_storage")),
})
.index("by_channel", ["channelId"])
.searchIndex("search_body", { searchField: "body", filterFields: ["channelId"] }),
});
// 复合索引:等值字段在前、范围字段在后
// 向量索引:.vectorIndex("by_embedding", { vectorField: "embedding", dimensions: 1536 })
响应式 query + 用索引(convex/messages.ts):
import { query } from "./_generated/server";
import { v } from "convex/values";
export const getByChannel = query({
args: { channelId: v.id("channels") },
handler: async (ctx, args) =>
ctx.db.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.order("desc").take(50), // 用 take/paginate,勿对大表裸 collect
});
Mutation(多文档自动原子):
export const transferCredits = mutation({
args: { fromUserId: v.id("users"), toUserId: v.id("users"), amount: v.number() },
handler: async (ctx, args) => {
const from = await ctx.db.get(args.fromUserId);
const to = await ctx.db.get(args.toUserId);
if (!from || !to) throw new Error("User not found");
if (from.credits < args.amount) throw new Error("Insufficient credits");
await ctx.db.patch(args.fromUserId, { credits: from.credits - args.amount });
await ctx.db.patch(args.toUserId, { credits: to.credits + args.amount });
},
});
Action 调外部 API,再经 mutation 回写(action 内不能直接 ctx.db):
export const sendEmail = action({
args: { to: v.string(), subject: v.string(), body: v.string() },
handler: async (ctx, args) => {
const res = await fetch("https://api.sendgrid.com/v3/mail/send", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.SENDGRID_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ /* ... */ }),
});
if (!res.ok) throw new Error("Failed to send email");
await ctx.runMutation(api.emails.recordSent, { to: args.to, sentAt: Date.now() });
},
});
客户端订阅(React/Next.js,useQuery 自动随数据更新):
"use client";
import { useQuery, useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";
export function TaskList() {
const tasks = useQuery(api.tasks.list); // undefined = 加载中(非空数据)
const addTask = useMutation(api.tasks.create);
if (tasks === undefined) return <p>Loading...</p>;
return <button onClick={() => addTask({ text: "New task", isCompleted: false })}>Add</button>;
}
分页用 usePaginatedQuery(api.x, {}, { initialNumItems: 20 }),配合 .paginate(paginationOpts)。
鉴权(原生 @convex-dev/auth,含 Magic Link/密码/80+ OAuth):用 ConvexAuthProvider 包裹,客户端 const { signIn } = useAuthActions() → signIn("github");query 内 await ctx.auth.getUserIdentity() 判断身份,未登录返回 null。第三方可用 ConvexProviderWithClerk。
定时与文件存储:
// 一次性:ctx.scheduler.runAfter(delayMs, api.notifications.send, {...})
// convex/crons.ts
import { cronJobs } from "convex/server";
const crons = cronJobs();
crons.interval("clear old logs", { hours: 24 }, api.logs.clearOld);
crons.cron("weekly digest", "0 9 * * 1", api.emails.sendWeeklyDigest);
export default crons;
// 文件:mutation 里 ctx.storage.generateUploadUrl() 取上传 URL,
// 存 storageId(v.id("_storage")),query 里 ctx.storage.getUrl(storageId) 读取。
注意事项
- 务必定义 Schema 和索引:Schema 提供端到端类型安全,是 Convex 的核心价值;查询优先
.withIndex()而非.filter()(后者全表扫描)。 - 外部 API 只能在 action 调;query/mutation 跑在确定性事务引擎里。action 内禁止直接
ctx.db,须经ctx.runQuery/ctx.runMutation。 process.env仅在 action 可读,query/mutation 中为 undefined。Date.now()、Math.random()在 query/mutation 中安全:Convex 在每次函数执行开始冻结时间。- 大表勿裸
.collect(),用.take(N)或.paginate();大二进制走文件存储,单文档上限 1MB。 - 文档引用用
v.id("tableName")而非裸字符串;找不到文档时倾向返回null而非抛错。 - 防无限循环:避免 action→mutation→调度 action 的环形
runQuery/runMutation链。 - 常见排错:query 首渲染返回
undefined属正常(加载态);mutation 报 Document not found 多因乐观并发下文档被删,应在 mutation 内重读;Schema 推送因存量数据失败时,先迁移数据或对新字段用v.optional()。 - 局限:无原生 SQL、无 SSR(需用 preloading 等模式)、有函数执行时长上限。
互见
- 同域可对比的后端/数据方案:Firebase(Firestore)、Supabase(PostgreSQL)、Prisma(ORM)——Convex 是 TypeScript 优先、文档-关系型且内建响应式。
- 前端配套:Next.js App Router、React hooks 模式。
- 鉴权与支付:OAuth/Clerk/Auth0/Convex Auth;Stripe 经 action 与 HTTP webhook 集成。
- 官方资源:docs.convex.dev、stack.convex.dev、github.com/get-convex/convex-backend。
采编自 sickn33/antigravity-awesome-skills(MIT)。