convex-pro-max

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Convex Pro Max

Convex Pro Max

The definitive guide for building production-ready Convex applications.
构建生产级Convex应用的权威指南。

Critical Rules

核心规则

  1. Always use new function syntax with
    args
    ,
    returns
    , and
    handler
    .
  2. Always validate args AND returns on public functions.
  3. Always use indexes — never
    .filter()
    on the database. Use
    .withIndex()
    .
  4. Always
    await
    promises
    — enable
    @typescript-eslint/no-floating-promises
    .
  5. Use
    internal*
    functions
    for scheduled jobs, crons, and sensitive operations.
  6. Never use
    ctx.db
    in actions
    — use
    ctx.runQuery
    /
    ctx.runMutation
    .
  7. Actions are NOT transactional — consolidate reads into single queries, writes into single mutations.
  8. Return
    null
    explicitly
    if a function returns nothing (
    returns: v.null()
    ).
  9. Use
    v.id("table")
    not
    v.string()
    for document IDs.
  10. Install
    @convex-dev/eslint-plugin
    — enforces object syntax, arg validators, explicit table IDs, correct runtime imports.
  1. 始终使用新函数语法,包含
    args
    returns
    handler
  2. 始终进行校验:公共函数的参数和返回值都要校验。
  3. 始终使用索引——绝不在数据库中使用
    .filter()
    ,改用
    .withIndex()
  4. 始终
    await
    Promise
    ——启用
    @typescript-eslint/no-floating-promises
    规则。
  5. 使用
    internal*
    函数
    处理定时任务、Cron任务和敏感操作。
  6. 绝不在Action中使用
    ctx.db
    ——改用
    ctx.runQuery
    /
    ctx.runMutation
  7. Action不支持事务——将读取操作合并到单个查询中,写入操作合并到单个变更中。
  8. 如果函数无返回值,显式返回
    null
    (使用
    returns: v.null()
    )。
  9. **使用
    v.id("table")
    **而非
    v.string()
    来表示文档ID。
  10. 安装
    @convex-dev/eslint-plugin
    ——强制使用对象语法、参数校验器、显式表ID、正确的运行时导入。

Function Types

函数类型

TypeDB AccessExternal APIsTransactionalCached/Reactive
query
Read-only via
ctx.db
NoYesYes
mutation
Read/Write via
ctx.db
NoYesNo
action
Via
runQuery
/
runMutation
YesNoNo
httpAction
Via
runQuery
/
runMutation
YesNoNo
类型数据库访问权限外部API调用事务支持缓存/响应式
query
仅可读(通过
ctx.db
不支持支持支持
mutation
可读可写(通过
ctx.db
不支持支持不支持
action
通过
runQuery
/
runMutation
支持不支持不支持
httpAction
通过
runQuery
/
runMutation
支持不支持不支持

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 functions
Only 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:
    by_channel_and_author
    also serves queries by
    channelId
    alone.
  • Don't create
    by_channel
    separately if
    by_channel_and_author
    already exists.
  • Name convention:
    by_field1_and_field2
    .
  • Nested fields use dot notation:
    .index("by_theme", ["settings.theme"])
    .
  • System fields
    _id
    and
    _creationTime
    are automatically available.
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
    ,
    transferOwnership
    ) instead of one generic
    updateTeam
    .
  • Use
    internalMutation
    for crons, scheduled jobs, webhooks — clients cannot call them.
  • 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
    ConvexError
    data is forwarded; other errors show "Server Error".
  • 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 registration
Use plain TypeScript functions in
model/
instead of
ctx.runAction
for code organization. Only use
ctx.runAction
when calling Convex components or crossing runtimes.
convex/
├── 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       # 组件注册
model/
目录中使用纯TypeScript函数
,而非
ctx.runAction
来组织代码。仅在调用Convex组件或跨运行时调用时使用
ctx.runAction

Reference 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