convex-pro-max
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseConvex Pro Max
Convex Pro Max
The definitive guide for building production-ready Convex applications.
构建生产级Convex应用的权威指南。
Critical Rules
核心规则
- Always use new function syntax with ,
args, andreturns.handler - Always validate args AND returns on public functions.
- Always use indexes — never on the database. Use
.filter()..withIndex() - Always promises — enable
await.@typescript-eslint/no-floating-promises - Use functions for scheduled jobs, crons, and sensitive operations.
internal* - Never use in actions — use
ctx.db/ctx.runQuery.ctx.runMutation - Actions are NOT transactional — consolidate reads into single queries, writes into single mutations.
- Return explicitly if a function returns nothing (
null).returns: v.null() - Use not
v.id("table")for document IDs.v.string() - Install — enforces object syntax, arg validators, explicit table IDs, correct runtime imports.
@convex-dev/eslint-plugin
- 始终使用新函数语法,包含、
args和returns。handler - 始终进行校验:公共函数的参数和返回值都要校验。
- 始终使用索引——绝不在数据库中使用,改用
.filter()。.withIndex() - 始终Promise——启用
await规则。@typescript-eslint/no-floating-promises - 使用函数处理定时任务、Cron任务和敏感操作。
internal* - 绝不在Action中使用——改用
ctx.db/ctx.runQuery。ctx.runMutation - Action不支持事务——将读取操作合并到单个查询中,写入操作合并到单个变更中。
- 如果函数无返回值,显式返回(使用
null)。returns: v.null() - **使用**而非
v.id("table")来表示文档ID。v.string() - 安装——强制使用对象语法、参数校验器、显式表ID、正确的运行时导入。
@convex-dev/eslint-plugin
Function Types
函数类型
| Type | DB Access | External APIs | Transactional | Cached/Reactive |
|---|---|---|---|---|
| Read-only via | No | Yes | Yes |
| Read/Write via | No | Yes | No |
| Via | Yes | No | No |
| Via | Yes | No | No |
| 类型 | 数据库访问权限 | 外部API调用 | 事务支持 | 缓存/响应式 |
|---|---|---|---|---|
| 仅可读(通过 | 不支持 | 支持 | 支持 |
| 可读可写(通过 | 不支持 | 支持 | 不支持 |
| 通过 | 支持 | 不支持 | 不支持 |
| 通过 | 支持 | 不支持 | 不支持 |
Query
Query
typescript
import { query } from "./_generated/server";
import { v } from "convex/values";
export const getUser = query({
args: { userId: v.id("users") },
returns: v.union(v.object({
_id: v.id("users"), _creationTime: v.number(),
name: v.string(), email: v.string(),
}), v.null()),
handler: async (ctx, args) => {
return await ctx.db.get(args.userId);
},
});typescript
import { query } from "./_generated/server";
import { v } from "convex/values";
export const getUser = query({
args: { userId: v.id("users") },
returns: v.union(v.object({
_id: v.id("users"), _creationTime: v.number(),
name: v.string(), email: v.string(),
}), v.null()),
handler: async (ctx, args) => {
return await ctx.db.get(args.userId);
},
});Mutation
Mutation
typescript
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const createTask = mutation({
args: { title: v.string(), userId: v.id("users") },
returns: v.id("tasks"),
handler: async (ctx, args) => {
return await ctx.db.insert("tasks", {
title: args.title, userId: args.userId,
completed: false, createdAt: Date.now(),
});
},
});typescript
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const createTask = mutation({
args: { title: v.string(), userId: v.id("users") },
returns: v.id("tasks"),
handler: async (ctx, args) => {
return await ctx.db.insert("tasks", {
title: args.title, userId: args.userId,
completed: false, createdAt: Date.now(),
});
},
});Action (external APIs)
Action(外部API调用)
typescript
"use node"; // Required for Node.js APIs
import { action } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";
export const processPayment = action({
args: { orderId: v.id("orders"), amount: v.number() },
returns: v.null(),
handler: async (ctx, args) => {
const order = await ctx.runQuery(internal.orders.get, { orderId: args.orderId });
const result = await fetch("https://api.stripe.com/...", { method: "POST", /* ... */ });
await ctx.runMutation(internal.orders.updateStatus, {
orderId: args.orderId, status: result.ok ? "paid" : "failed",
});
return null;
},
});typescript
"use node"; // 调用Node.js API时必填
import { action } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";
export const processPayment = action({
args: { orderId: v.id("orders"), amount: v.number() },
returns: v.null(),
handler: async (ctx, args) => {
const order = await ctx.runQuery(internal.orders.get, { orderId: args.orderId });
const result = await fetch("https://api.stripe.com/...", { method: "POST", /* ... */ });
await ctx.runMutation(internal.orders.updateStatus, {
orderId: args.orderId, status: result.ok ? "paid" : "failed",
});
return null;
},
});Internal functions
内部函数
typescript
import { internalMutation, internalQuery, internalAction } from "./_generated/server";
import { internal } from "./_generated/api"; // for referencing internal functions
import { api } from "./_generated/api"; // for referencing public functionsOnly callable by other Convex functions, crons, and the dashboard — never by clients.
typescript
import { internalMutation, internalQuery, internalAction } from "./_generated/server";
import { internal } from "./_generated/api"; // 引用内部函数
import { api } from "./_generated/api"; // 引用公共函数仅可被其他Convex函数、Cron任务和控制台调用——客户端无法调用。
Schema & Indexes
Schema与索引
typescript
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
users: defineTable({
name: v.string(),
email: v.string(),
role: v.union(v.literal("admin"), v.literal("member")),
settings: v.object({ theme: v.union(v.literal("light"), v.literal("dark")) }),
})
.index("by_email", ["email"])
.index("by_role", ["role"]),
messages: defineTable({
channelId: v.id("channels"),
authorId: v.id("users"),
content: v.string(),
})
.index("by_channel", ["channelId"])
.index("by_channel_and_author", ["channelId", "authorId"])
.searchIndex("search_content", { searchField: "content", filterFields: ["channelId"] }),
});Index rules:
- Compound indexes are prefix-searchable: also serves queries by
by_channel_and_authoralone.channelId - Don't create separately if
by_channelalready exists.by_channel_and_author - Name convention: .
by_field1_and_field2 - Nested fields use dot notation: .
.index("by_theme", ["settings.theme"]) - System fields and
_idare automatically available._creationTime
typescript
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
users: defineTable({
name: v.string(),
email: v.string(),
role: v.union(v.literal("admin"), v.literal("member")),
settings: v.object({ theme: v.union(v.literal("light"), v.literal("dark")) }),
})
.index("by_email", ["email"])
.index("by_role", ["role"]),
messages: defineTable({
channelId: v.id("channels"),
authorId: v.id("users"),
content: v.string(),
})
.index("by_channel", ["channelId"])
.index("by_channel_and_author", ["channelId", "authorId"])
.searchIndex("search_content", { searchField: "content", filterFields: ["channelId"] }),
});索引规则:
- 复合索引支持前缀搜索:同样可用于仅按
by_channel_and_author查询的场景。channelId - 如果已存在,无需单独创建
by_channel_and_author索引。by_channel - 命名规范:。
by_field1_and_field2 - 嵌套字段使用点表示法:。
.index("by_theme", ["settings.theme"]) - 系统字段和
_id自动可用。_creationTime
Database Operations
数据库操作
typescript
// Read
const doc = await ctx.db.get(id); // by ID (null if not found)
const docs = await ctx.db.query("table").collect(); // all (bounded)
const first = await ctx.db.query("table").first(); // first or null
const one = await ctx.db.query("table").withIndex(...).unique(); // exactly one (throws if 0 or >1)
const top10 = await ctx.db.query("table").order("desc").take(10);
// Indexed query
const results = await ctx.db.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", channelId))
.order("desc")
.take(50);
// Write
const id = await ctx.db.insert("table", { ...fields });
await ctx.db.patch(id, { field: newValue }); // partial update
await ctx.db.replace(id, { ...allFields }); // full replace (keeps _id, _creationTime)
await ctx.db.delete(id);typescript
// 读取操作
const doc = await ctx.db.get(id); // 根据ID查询(不存在则返回null)
const docs = await ctx.db.query("table").collect(); // 查询所有数据(有数量限制)
const first = await ctx.db.query("table").first(); // 查询第一条数据(不存在则返回null)
const one = await ctx.db.query("table").withIndex(...).unique(); // 查询唯一匹配数据(无匹配或多个匹配时抛出异常)
const top10 = await ctx.db.query("table").order("desc").take(10); // 查询前10条数据(降序)
// 带索引的查询
const results = await ctx.db.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", channelId))
.order("desc")
.take(50);
// 写入操作
const id = await ctx.db.insert("table", { ...fields }); // 插入数据
await ctx.db.patch(id, { field: newValue }); // 部分更新
await ctx.db.replace(id, { ...allFields }); // 完全替换(保留_id和_creationTime)
await ctx.db.delete(id); // 删除数据Validators Quick Reference
校验器速查
typescript
v.string() v.number() v.boolean() v.null()
v.id("tableName") v.int64() v.bytes() v.any()
v.array(v.string()) v.record(v.string(), v.number())
v.object({ name: v.string(), age: v.optional(v.number()) })
v.union(v.literal("a"), v.literal("b")) // enum-like
v.optional(v.string()) // field can be omitted
v.nullable(v.string()) // shorthand for v.union(v.string(), v.null())Reusable validators:
typescript
const roleValidator = v.union(v.literal("admin"), v.literal("member"));
const profileValidator = v.object({ name: v.string(), bio: v.optional(v.string()) });Extract TypeScript types:
typescript
import { Infer } from "convex/values";
type Role = Infer<typeof roleValidator>; // "admin" | "member"Generated types:
typescript
import { Doc, Id } from "./_generated/dataModel";
import { QueryCtx, MutationCtx, ActionCtx } from "./_generated/server";typescript
v.string() v.number() v.boolean() v.null()
v.id("tableName") v.int64() v.bytes() v.any()
v.array(v.string()) v.record(v.string(), v.number())
v.object({ name: v.string(), age: v.optional(v.number()) })
v.union(v.literal("a"), v.literal("b")) // 枚举风格
v.optional(v.string()) // 字段可省略
v.nullable(v.string()) // v.union(v.string(), v.null())的简写可复用校验器:
typescript
const roleValidator = v.union(v.literal("admin"), v.literal("member"));
const profileValidator = v.object({ name: v.string(), bio: v.optional(v.string()) });提取TypeScript类型:
typescript
import { Infer } from "convex/values";
type Role = Infer<typeof roleValidator>; // "admin" | "member"自动生成的类型:
typescript
import { Doc, Id } from "./_generated/dataModel";
import { QueryCtx, MutationCtx, ActionCtx } from "./_generated/server";Authentication & Security
身份验证与安全
typescript
// Reusable auth helper
export async function getCurrentUser(ctx: QueryCtx | MutationCtx) {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new ConvexError({ code: "UNAUTHORIZED", message: "Must be logged in" });
const user = await ctx.db.query("users")
.withIndex("by_token", (q) => q.eq("tokenIdentifier", identity.tokenIdentifier))
.unique();
if (!user) throw new ConvexError({ code: "USER_NOT_FOUND", message: "User not found" });
return user;
}Security rules:
- Never trust client-provided identifiers — derive user from .
ctx.auth - Use granular mutations (separate ,
setTeamName) instead of one generictransferOwnership.updateTeam - Use for crons, scheduled jobs, webhooks — clients cannot call them.
internalMutation - Convex IDs are unguessable, but still verify authorization.
typescript
// 可复用的身份验证工具函数
export async function getCurrentUser(ctx: QueryCtx | MutationCtx) {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new ConvexError({ code: "UNAUTHORIZED", message: "请先登录" });
const user = await ctx.db.query("users")
.withIndex("by_token", (q) => q.eq("tokenIdentifier", identity.tokenIdentifier))
.unique();
if (!user) throw new ConvexError({ code: "USER_NOT_FOUND", message: "用户不存在" });
return user;
}安全规则:
- 绝不信任客户端提供的标识符——从中获取用户信息。
ctx.auth - 使用细粒度的变更函数(如单独的、
setTeamName),而非通用的transferOwnership函数。updateTeam - 对于Cron任务、定时任务、Webhook,使用——客户端无法调用这些函数。
internalMutation - Convex ID不可猜测,但仍需验证权限。
Error Handling
错误处理
typescript
import { ConvexError } from "convex/values";
// Throw structured errors
throw new ConvexError({ code: "NOT_FOUND", message: "Task not found" });
// Client catches
try { await createUser({ email }); }
catch (error) {
if (error instanceof ConvexError) { /* error.data.code, error.data.message */ }
}- Dev: full error messages sent to client. Prod: only data is forwarded; other errors show "Server Error".
ConvexError - Mutation errors roll back the entire transaction.
typescript
import { ConvexError } from "convex/values";
// 抛出结构化错误
throw new ConvexError({ code: "NOT_FOUND", message: "任务不存在" });
// 客户端捕获错误
try { await createUser({ email }); }
catch (error) {
if (error instanceof ConvexError) { /* error.data.code, error.data.message */ }
}- 开发环境:完整错误信息会发送到客户端。生产环境:仅的自定义数据会返回给客户端;其他错误会显示“服务器错误”。
ConvexError - 变更函数执行出错时,整个事务会回滚。
Code Organization
代码组织
convex/
├── schema.ts # Schema + indexes
├── auth.ts # getCurrentUser, requireTeamMember helpers
├── users.ts # Public user API (thin wrappers)
├── teams.ts # Public team API
├── model/
│ ├── users.ts # User business logic (pure TS functions)
│ └── teams.ts # Team business logic
├── http.ts # HTTP actions (webhooks, APIs)
├── crons.ts # Cron jobs
└── convex.config.ts # Component registrationUse plain TypeScript functions in instead of for code organization.
Only use when calling Convex components or crossing runtimes.
model/ctx.runActionctx.runActionconvex/
├── schema.ts # Schema与索引
├── auth.ts # getCurrentUser、权限校验等工具函数
├── users.ts # 公共用户API(轻量封装)
├── teams.ts # 公共团队API
├── model/
│ ├── users.ts # 用户业务逻辑(纯TypeScript函数)
│ └── teams.ts # 团队业务逻辑
├── http.ts # HTTP动作(Webhook、外部API)
├── crons.ts # Cron任务
└── convex.config.ts # 组件注册在目录中使用纯TypeScript函数,而非来组织代码。仅在调用Convex组件或跨运行时调用时使用。
model/ctx.runActionctx.runActionReference Files
参考文档
- HTTP actions, scheduling, crons, file storage, runtimes: See references/functions-deep.md
- Denormalization, OCC, sharding, N+1, aggregates, query optimization: See references/performance.md
- Common patterns, anti-patterns, testing, migration: See references/patterns.md
- HTTP动作、任务调度、Cron任务、文件存储、运行时:参见references/functions-deep.md
- 反规范化、OCC、分片、N+1问题、聚合操作、查询优化:参见references/performance.md
- 常见模式、反模式、测试、迁移:参见references/patterns.md