convex-fundamentals

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Convex Fundamentals for TypeScript Agents

面向TypeScript开发者的Convex基础教程

Overview

概述

Convex is a reactive backend-as-a-service that provides real-time data synchronization, ACID transactions, and TypeScript-first development. This skill covers the core mental model, function types, layered architecture patterns, and HTTP action implementation.
Convex是一款响应式后端即服务(BaaS),提供实时数据同步、ACID事务以及TypeScript优先的开发体验。本技能涵盖核心思维模型、函数类型、分层架构模式以及HTTP action的实现方法。

TypeScript: NEVER Use
any
Type

TypeScript:禁止使用
any
类型

CRITICAL RULE: This codebase has
@typescript-eslint/no-explicit-any
enabled. Using
any
will cause build failures.
❌ WRONG:
typescript
function handleData(data: any) { ... }
const items: any[] = [];
✅ CORRECT:
typescript
function handleData(data: { id: string; name: string }) { ... }
const items: Doc<"items">[] = [];
【重要规则】:本代码库已启用
@typescript-eslint/no-explicit-any
规则,使用
any
会导致构建失败。
❌ 错误示例:
typescript
function handleData(data: any) { ... }
const items: any[] = [];
✅ 正确示例:
typescript
function handleData(data: { id: string; name: string }) { ... }
const items: Doc<"items">[] = [];

When to Use This Skill

适用场景

Use this skill when:
  • Building Convex backend applications from scratch
  • Creating queries, mutations, or actions
  • Understanding the difference between function types
  • Implementing layered architecture patterns
  • Setting up HTTP webhooks with route handlers
  • Migrating from other backends to Convex
  • Understanding Convex's reactive data model
在以下场景中使用本技能:
  • 从零开始构建Convex后端应用
  • 创建query、mutation或action
  • 理解不同函数类型的差异
  • 实现分层架构模式
  • 通过路由处理器设置HTTP Webhook
  • 从其他后端迁移至Convex
  • 理解Convex的响应式数据模型

Philosophy: Why Convex Feels "Wrong" (And Why That's Right)

设计理念:为何Convex看似“反直觉”(实则合理)

Convex intentionally constrains you. When something feels like an anti-pattern, it's usually Convex telling you to rethink the approach.
Convex会刻意对开发者加以约束。当你觉得某种写法是反模式时,通常是Convex在提示你重新思考实现方式。

The Intentional Friction

刻意设计的约束

What feels wrongWhat Convex is telling you
"I can't use
.filter()
on queries"
Define an index. Queries should be O(log n), not O(n).
"Actions can't access
ctx.db
"
Side effects and transactions don't mix. Route through mutations.
"My mutation keeps failing with OCC errors"You're writing too often to the same document. Redesign your data model or use Workpool.
"I can't call
fetch()
in a mutation"
Mutations must be deterministic. Schedule an action instead.
"Queries re-run on every document change"You're collecting too much data. Narrow your index or denormalize.
"I can't do joins"Denormalize. Embed related data or use lookup tables.
看似反直觉的操作Convex的深层提示
“我无法在query中使用
.filter()
请定义索引。Query应保证O(log n)的时间复杂度,而非O(n)。
“Action无法访问
ctx.db
副作用与事务不能混合使用,请通过mutation处理。
“我的mutation频繁出现OCC错误”你过于频繁地写入同一文档。请重新设计数据模型或使用Workpool。
“我无法在mutation中调用
fetch()
Mutation必须是确定性的,请改用action执行。
“每次文档变更时Query都会重新运行”你查询的数据范围过大,请缩小索引范围或使用反范式设计。
“我无法执行关联查询”使用反范式设计,嵌入关联数据或使用查找表。

Core Invariants

核心不变原则

  1. Mutations are ACID transactions — All writes succeed together or fail together.
  2. Queries are reactive — They re-run when any read document changes.
  3. Actions are non-transactional orchestration — No reactivity, no
    ctx.db
    . Can do external I/O and must call queries/mutations via
    ctx.run*
    .
  4. Scheduling is atomic with mutations — If mutation fails, scheduled functions don't run.
  1. Mutation是ACID事务 — 所有写入操作要么全部成功,要么全部失败。
  2. Query是响应式的 — 当任何读取的文档发生变更时,Query会重新运行。
  3. Action是非事务性的编排工具 — 无响应式特性,无法访问
    ctx.db
    ,可执行外部I/O操作,必须通过
    ctx.run*
    调用query/mutation。
  4. 调度与mutation原子性绑定 — 如果mutation执行失败,已调度的函数不会运行。

Core Mental Model

核心思维模型

Everything in Convex falls into these categories:
KindVisibilityPurpose
query
publicRead from DB, reactive subscriptions
mutation
publicWrite to DB, ACID transactions
action
publicExternal APIs, non-deterministic work
httpAction
public (HTTP)Webhooks, third-party integrations
internalQuery
privateRead primitives for internal use
internalMutation
privateWrite primitives for internal use
internalAction
privateOrchestration, external calls
Rule: Export a thin public surface. All real logic lives in internal primitives.
internal*
variants have identical execution semantics to their public counterparts. The only difference is visibility — internal functions appear on
internal.*
, not
api.*
, and cannot be called from clients.
Convex中的所有功能可分为以下类别:
类型可见性用途
query
公开读取数据库,响应式订阅
mutation
公开写入数据库,ACID事务
action
公开调用外部API,执行非确定性任务
httpAction
公开(HTTP)Webhook、第三方集成
internalQuery
私有供内部使用的读取基础组件
internalMutation
私有供内部使用的写入基础组件
internalAction
私有任务编排、外部API调用
规则:仅对外暴露精简的公共接口,所有核心逻辑均实现为内部基础组件。
internal*
系列组件的执行语义与对应的公共组件完全一致,唯一区别是可见性——内部函数挂载在
internal.*
下,而非
api.*
,且无法被客户端直接调用。

Layered Architecture

分层架构

Layer 1: Client Surface (Public API)

第一层:客户端接口(公共API)

Only entrypoints live here. Thin wrappers that validate, auth-check, and delegate.
typescript
// convex/jobs.ts — PUBLIC SURFACE
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { internal } from "./_generated/api";

// Public mutation: client entrypoint
export const startGeneration = mutation({
  args: { prompt: v.string() },
  returns: v.id("jobs"),
  handler: async (ctx, args) => {
    // 1. Auth check
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Unauthorized");

    // 2. Delegate to internal mutation
    const jobId = await ctx.runMutation(internal.jobs.createJob, {
      userId: identity.subject,
      prompt: args.prompt,
    });

    // 3. Schedule background work
    await ctx.scheduler.runAfter(0, internal.jobs.processJob, { jobId });

    return jobId;
  },
});

// Public query: reactive subscription
export const getJob = query({
  args: { jobId: v.id("jobs") },
  returns: v.union(
    v.object({
      _id: v.id("jobs"),
      _creationTime: v.number(),
      status: v.string(),
      prompt: v.string(),
      result: v.optional(v.string()),
    }),
    v.null()
  ),
  handler: async (ctx, args) => {
    return await ctx.db.get(args.jobId);
  },
});
仅包含入口点,是用于验证、权限检查和委托调用的轻量级包装器。
typescript
// convex/jobs.ts — 公共接口
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { internal } from "./_generated/api";

// 公开mutation:客户端入口
export const startGeneration = mutation({
  args: { prompt: v.string() },
  returns: v.id("jobs"),
  handler: async (ctx, args) => {
    // 1. 权限检查
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Unauthorized");

    // 2. 委托给内部mutation
    const jobId = await ctx.runMutation(internal.jobs.createJob, {
      userId: identity.subject,
      prompt: args.prompt,
    });

    // 3. 调度后台任务
    await ctx.scheduler.runAfter(0, internal.jobs.processJob, { jobId });

    return jobId;
  },
});

// 公开query:响应式订阅
export const getJob = query({
  args: { jobId: v.id("jobs") },
  returns: v.union(
    v.object({
      _id: v.id("jobs"),
      _creationTime: v.number(),
      status: v.string(),
      prompt: v.string(),
      result: v.optional(v.string()),
    }),
    v.null()
  ),
  handler: async (ctx, args) => {
    return await ctx.db.get(args.jobId);
  },
});

Layer 2: Domain Logic (Internal Primitives)

第二层:领域逻辑(内部基础组件)

Where the real logic lives. These are your backend's private API.
typescript
// convex/jobs.ts — INTERNAL PRIMITIVES (same file, different exports)
import { internalMutation, internalQuery } from "./_generated/server";
import { v } from "convex/values";

export const createJob = internalMutation({
  args: {
    userId: v.string(),
    prompt: v.string(),
  },
  returns: v.id("jobs"),
  handler: async (ctx, args) => {
    return await ctx.db.insert("jobs", {
      userId: args.userId,
      prompt: args.prompt,
      status: "pending",
    });
  },
});

export const markComplete = internalMutation({
  args: {
    jobId: v.id("jobs"),
    result: v.string(),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.patch(args.jobId, {
      status: "completed",
      result: args.result,
    });
    return null;
  },
});

export const markFailed = internalMutation({
  args: {
    jobId: v.id("jobs"),
    error: v.string(),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.patch(args.jobId, {
      status: "failed",
      error: args.error,
    });
    return null;
  },
});

export const getById = internalQuery({
  args: { jobId: v.id("jobs") },
  returns: v.union(
    v.object({
      _id: v.id("jobs"),
      _creationTime: v.number(),
      userId: v.string(),
      prompt: v.string(),
      status: v.string(),
      result: v.optional(v.string()),
      error: v.optional(v.string()),
    }),
    v.null()
  ),
  handler: async (ctx, args) => {
    return await ctx.db.get(args.jobId);
  },
});
核心逻辑实现层,是后端的私有API。
typescript
// convex/jobs.ts — 内部基础组件(同一文件,不同导出)
import { internalMutation, internalQuery } from "./_generated/server";
import { v } from "convex/values";

export const createJob = internalMutation({
  args: {
    userId: v.string(),
    prompt: v.string(),
  },
  returns: v.id("jobs"),
  handler: async (ctx, args) => {
    return await ctx.db.insert("jobs", {
      userId: args.userId,
      prompt: args.prompt,
      status: "pending",
    });
  },
});

export const markComplete = internalMutation({
  args: {
    jobId: v.id("jobs"),
    result: v.string(),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.patch(args.jobId, {
      status: "completed",
      result: args.result,
    });
    return null;
  },
});

export const markFailed = internalMutation({
  args: {
    jobId: v.id("jobs"),
    error: v.string(),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.patch(args.jobId, {
      status: "failed",
      error: args.error,
    });
    return null;
  },
});

export const getById = internalQuery({
  args: { jobId: v.id("jobs") },
  returns: v.union(
    v.object({
      _id: v.id("jobs"),
      _creationTime: v.number(),
      userId: v.string(),
      prompt: v.string(),
      status: v.string(),
      result: v.optional(v.string()),
      error: v.optional(v.string()),
    }),
    v.null()
  ),
  handler: async (ctx, args) => {
    return await ctx.db.get(args.jobId);
  },
});

Layer 3: Orchestration (Actions + Scheduler)

第三层:任务编排(Action + 调度器)

For multi-step, external, or long-running work.
typescript
// convex/jobs.ts — ORCHESTRATION
import { internalAction } from "./_generated/server";
import { v } from "convex/values";
import { internal } from "./_generated/api";

export const processJob = internalAction({
  args: { jobId: v.id("jobs") },
  returns: v.null(),
  handler: async (ctx, args) => {
    // 1. Read current state
    const job = await ctx.runQuery(internal.jobs.getById, {
      jobId: args.jobId,
    });
    if (!job || job.status !== "pending") return null;

    try {
      // 2. External API call (non-deterministic)
      const result = await fetch("https://api.example.com/generate", {
        method: "POST",
        body: JSON.stringify({ prompt: job.prompt }),
      });
      const data = await result.json();

      // 3. Write result via mutation
      await ctx.runMutation(internal.jobs.markComplete, {
        jobId: args.jobId,
        result: data.output,
      });
    } catch (e) {
      await ctx.runMutation(internal.jobs.markFailed, {
        jobId: args.jobId,
        error: String(e),
      });
    }

    return null;
  },
});
用于处理多步骤、外部调用或长时间运行的任务。
typescript
// convex/jobs.ts — 任务编排
import { internalAction } from "./_generated/server";
import { v } from "convex/values";
import { internal } from "./_generated/api";

export const processJob = internalAction({
  args: { jobId: v.id("jobs") },
  returns: v.null(),
  handler: async (ctx, args) => {
    // 1. 读取当前状态
    const job = await ctx.runQuery(internal.jobs.getById, {
      jobId: args.jobId,
    });
    if (!job || job.status !== "pending") return null;

    try {
      // 2. 调用外部API(非确定性操作)
      const result = await fetch("https://api.example.com/generate", {
        method: "POST",
        body: JSON.stringify({ prompt: job.prompt }),
      });
      const data = await result.json();

      // 3. 通过mutation写入结果
      await ctx.runMutation(internal.jobs.markComplete, {
        jobId: args.jobId,
        result: data.output,
      });
    } catch (e) {
      await ctx.runMutation(internal.jobs.markFailed, {
        jobId: args.jobId,
        error: String(e),
      });
    }

    return null;
  },
});

HTTP Actions (Webhooks)

HTTP Action(Webhook)

HTTP actions must be in
convex/http.ts
with
httpRouter
:
typescript
// convex/http.ts
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import { internal } from "./_generated/api";

const http = httpRouter();

http.route({
  path: "/webhooks/stripe",
  method: "POST",
  handler: httpAction(async (ctx, req) => {
    // 1. Validate signature
    const signature = req.headers.get("stripe-signature");
    if (!signature) {
      return new Response("Missing signature", { status: 401 });
    }

    // 2. Parse body
    const body = await req.text();

    // 3. Delegate to internal mutation
    await ctx.runMutation(internal.billing.handleStripeWebhook, {
      signature,
      body,
    });

    return new Response(null, { status: 200 });
  }),
});

export default http;
HTTP action必须定义在
convex/http.ts
中,并使用
httpRouter
typescript
// convex/http.ts
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import { internal } from "./_generated/api";

const http = httpRouter();

http.route({
  path: "/webhooks/stripe",
  method: "POST",
  handler: httpAction(async (ctx, req) => {
    // 1. 验证签名
    const signature = req.headers.get("stripe-signature");
    if (!signature) {
      return new Response("Missing signature", { status: 401 });
    }

    // 2. 解析请求体
    const body = await req.text();

    // 3. 委托给内部mutation
    await ctx.runMutation(internal.billing.handleStripeWebhook, {
      signature,
      body,
    });

    return new Response(null, { status: 200 });
  }),
});

export default http;

Model Layer Pattern

模型层模式

Extract business logic into reusable model functions. Functions stay thin; models do the work.
将业务逻辑提取为可复用的模型函数。保持函数轻量化,核心工作由模型完成。

Why Model Layer?

为何使用模型层?

  1. Reuse — Same logic across queries, mutations, actions
  2. Testability — Pure functions, easy to unit test
  3. Separation — Auth checks in one place, business logic in another
  4. Type Safety
    QueryCtx
    and
    MutationCtx
    types for context
  1. 复用性 — 同一逻辑可在query、mutation、action中共享
  2. 可测试性 — 纯函数,易于单元测试
  3. 关注点分离 — 权限检查集中处理,业务逻辑独立实现
  4. 类型安全 — 使用
    QueryCtx
    MutationCtx
    类型约束上下文

Pattern: Model Functions

模式:模型函数

typescript
// convex/model/users.ts — MODEL LAYER
import { QueryCtx, MutationCtx } from "../_generated/server";
import { Id } from "../_generated/dataModel";

// Read helper: reusable across queries
export async function getCurrentUser(ctx: QueryCtx) {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) throw new Error("Unauthorized");

  const user = await ctx.db
    .query("users")
    .withIndex("by_tokenIdentifier", (q) =>
      q.eq("tokenIdentifier", identity.tokenIdentifier)
    )
    .unique();

  if (!user) throw new Error("User not found");
  return user;
}

// Access control helper
export async function ensureHasAccess(
  ctx: QueryCtx,
  { conversationId }: { conversationId: Id<"conversations"> }
) {
  const user = await getCurrentUser(ctx);
  const conversation = await ctx.db.get(conversationId);
  if (!conversation || !conversation.members.includes(user._id)) {
    throw new Error("Unauthorized");
  }
  return { user, conversation };
}

// Write helper: encapsulates business logic
export async function addMessage(
  ctx: MutationCtx,
  {
    conversationId,
    content,
  }: { conversationId: Id<"conversations">; content: string }
) {
  const { user } = await ensureHasAccess(ctx, { conversationId });

  const messageId = await ctx.db.insert("messages", {
    conversationId,
    authorId: user._id,
    content,
    createdAt: Date.now(),
  });

  // Update denormalized lastMessageAt
  await ctx.db.patch(conversationId, { lastMessageAt: Date.now() });

  return messageId;
}
typescript
// convex/model/users.ts — 模型层
import { QueryCtx, MutationCtx } from "../_generated/server";
import { Id } from "../_generated/dataModel";

// 读取助手:可在多个query中复用
export async function getCurrentUser(ctx: QueryCtx) {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) throw new Error("Unauthorized");

  const user = await ctx.db
    .query("users")
    .withIndex("by_tokenIdentifier", (q) =>
      q.eq("tokenIdentifier", identity.tokenIdentifier)
    )
    .unique();

  if (!user) throw new Error("User not found");
  return user;
}

// 权限控制助手
export async function ensureHasAccess(
  ctx: QueryCtx,
  { conversationId }: { conversationId: Id<"conversations"> }
) {
  const user = await getCurrentUser(ctx);
  const conversation = await ctx.db.get(conversationId);
  if (!conversation || !conversation.members.includes(user._id)) {
    throw new Error("Unauthorized");
  }
  return { user, conversation };
}

// 写入助手:封装业务逻辑
export async function addMessage(
  ctx: MutationCtx,
  {
    conversationId,
    content,
  }: { conversationId: Id<"conversations">; content: string }
) {
  const { user } = await ensureHasAccess(ctx, { conversationId });

  const messageId = await ctx.db.insert("messages", {
    conversationId,
    authorId: user._id,
    content,
    createdAt: Date.now(),
  });

  // 更新反范式存储的lastMessageAt字段
  await ctx.db.patch(conversationId, { lastMessageAt: Date.now() });

  return messageId;
}

Pattern: Thin Function Wrappers

模式:轻量化函数包装器

typescript
// convex/conversations.ts — THIN WRAPPERS
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import * as Conversations from "./model/conversations";

// Query: delegates to model
export const listMessages = query({
  args: { conversationId: v.id("conversations") },
  returns: v.array(
    v.object({
      _id: v.id("messages"),
      content: v.string(),
      authorId: v.id("users"),
      createdAt: v.number(),
    })
  ),
  handler: async (ctx, args) => {
    return Conversations.listMessages(ctx, args);
  },
});

// Mutation: delegates to model
export const sendMessage = mutation({
  args: { conversationId: v.id("conversations"), content: v.string() },
  returns: v.id("messages"),
  handler: async (ctx, args) => {
    return Conversations.addMessage(ctx, args);
  },
});
typescript
// convex/conversations.ts — 轻量化包装器
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import * as Conversations from "./model/conversations";

// Query:委托给模型处理
export const listMessages = query({
  args: { conversationId: v.id("conversations") },
  returns: v.array(
    v.object({
      _id: v.id("messages"),
      content: v.string(),
      authorId: v.id("users"),
      createdAt: v.number(),
    })
  ),
  handler: async (ctx, args) => {
    return Conversations.listMessages(ctx, args);
  },
});

// Mutation:委托给模型处理
export const sendMessage = mutation({
  args: { conversationId: v.id("conversations"), content: v.string() },
  returns: v.id("messages"),
  handler: async (ctx, args) => {
    return Conversations.addMessage(ctx, args);
  },
});

File Structure

文件结构

Flat-first. Convex uses file-based routing:
convex/
  schema.ts              # Database schema
  http.ts                # HTTP actions (webhooks)
  auth.ts                # Auth helpers
  crons.ts               # Cron job definitions

  # Domain files (public + internal in same file)
  users.ts               # api.users.* + internal.users.*
  jobs.ts
  billing.ts
  audio.ts

  # Model layer (business logic)
  model/
    users.ts             # User business logic helpers
    conversations.ts     # Conversation logic
    billing.ts           # Billing logic

  # Shared utilities (NOT Convex functions)
  _lib/
    validators.ts        # Shared validators
    constants.ts         # Constants
Routing:
  • convex/users.ts
    api.users.functionName
    /
    internal.users.functionName
  • convex/audio/stems.ts
    api.audio.stems.functionName
  • convex/model/*.ts
    → NOT routed (helper imports only)
优先使用扁平化结构,Convex采用基于文件的路由机制:
convex/
  schema.ts              # 数据库 schema
  http.ts                # HTTP action(Webhook)
  auth.ts                # 权限验证助手
  crons.ts               # Cron 任务定义

  # 领域文件(公共与内部组件同文件)
  users.ts               # api.users.* + internal.users.*
  jobs.ts
  billing.ts
  audio.ts

  # 模型层(业务逻辑)
  model/
    users.ts             # 用户业务逻辑助手
    conversations.ts     # 会话逻辑
    billing.ts           # 账单逻辑

  # 共享工具库(非Convex函数)
  _lib/
    validators.ts        # 共享验证器
    constants.ts         # 常量定义
路由规则:
  • convex/users.ts
    api.users.functionName
    /
    internal.users.functionName
  • convex/audio/stems.ts
    api.audio.stems.functionName
  • convex/model/*.ts
    → 不参与路由(仅作为助手导入使用)

Function Types Quick Reference

函数类型速查

Import Patterns

导入方式

typescript
// Public (client-callable)
import { query, mutation, action } from "./_generated/server";

// Private (internal only)
import {
  internalQuery,
  internalMutation,
  internalAction,
} from "./_generated/server";

// HTTP
import { httpAction } from "./_generated/server";
import { httpRouter } from "convex/server";
typescript
// 公共组件(客户端可调用)
import { query, mutation, action } from "./_generated/server";

// 私有组件(仅内部使用)
import {
  internalQuery,
  internalMutation,
  internalAction,
} from "./_generated/server";

// HTTP组件
import { httpAction } from "./_generated/server";
import { httpRouter } from "convex/server";

Function References

函数引用方式

typescript
import { api, internal } from "./_generated/api";

// Public: api.fileName.functionName
await ctx.runQuery(api.users.getUser, { userId });

// Internal: internal.fileName.functionName
await ctx.runMutation(internal.jobs.createJob, { data });
typescript
import { api, internal } from "./_generated/api";

// 公共组件:api.fileName.functionName
await ctx.runQuery(api.users.getUser, { userId });

// 私有组件:internal.fileName.functionName
await ctx.runMutation(internal.jobs.createJob, { data });

Database Operations

数据库操作

typescript
// Read
const doc = await ctx.db.get(id);
const docs = await ctx.db
  .query("table")
  .withIndex("by_x", (q) => q.eq("x", value))
  .collect();
const single = await ctx.db
  .query("table")
  .withIndex("by_x", (q) => q.eq("x", value))
  .unique();

// Write
const id = await ctx.db.insert("table", { field: value });
await ctx.db.patch(id, { field: newValue });
await ctx.db.replace(id, { ...fullDocument });
await ctx.db.delete(id);
typescript
// 读取
const doc = await ctx.db.get(id);
const docs = await ctx.db
  .query("table")
  .withIndex("by_x", (q) => q.eq("x", value))
  .collect();
const single = await ctx.db
  .query("table")
  .withIndex("by_x", (q) => q.eq("x", value))
  .unique();

// 写入
const id = await ctx.db.insert("table", { field: value });
await ctx.db.patch(id, { field: newValue });
await ctx.db.replace(id, { ...fullDocument });
await ctx.db.delete(id);

Common Pitfalls

常见陷阱

Pitfall 1: Calling fetch() in Mutations

陷阱1:在Mutation中调用fetch()

❌ WRONG:
typescript
export const createOrder = mutation({
  args: { productId: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    // ❌ Mutations must be deterministic - no external calls!
    const price = await fetch(
      "https://api.stripe.com/prices/" + args.productId
    );
    await ctx.db.insert("orders", { productId: args.productId, price });
    return null;
  },
});
✅ CORRECT:
typescript
// Mutation creates order, schedules action for external call
export const createOrder = mutation({
  args: { productId: v.string() },
  returns: v.id("orders"),
  handler: async (ctx, args) => {
    const orderId = await ctx.db.insert("orders", {
      productId: args.productId,
      status: "pending",
    });
    await ctx.scheduler.runAfter(0, internal.orders.fetchPrice, { orderId });
    return orderId;
  },
});

// Action handles external API call
export const fetchPrice = internalAction({
  args: { orderId: v.id("orders") },
  returns: v.null(),
  handler: async (ctx, args) => {
    const order = await ctx.runQuery(internal.orders.getById, {
      orderId: args.orderId,
    });
    const price = await fetch(
      "https://api.stripe.com/prices/" + order.productId
    );
    await ctx.runMutation(internal.orders.updatePrice, {
      orderId: args.orderId,
      price: await price.json(),
    });
    return null;
  },
});
❌ 错误示例:
typescript
export const createOrder = mutation({
  args: { productId: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    // ❌ Mutation必须是确定性的,禁止调用外部API!
    const price = await fetch(
      "https://api.stripe.com/prices/" + args.productId
    );
    await ctx.db.insert("orders", { productId: args.productId, price });
    return null;
  },
});
✅ 正确示例:
typescript
// Mutation创建订单,调度Action执行外部调用
export const createOrder = mutation({
  args: { productId: v.string() },
  returns: v.id("orders"),
  handler: async (ctx, args) => {
    const orderId = await ctx.db.insert("orders", {
      productId: args.productId,
      status: "pending",
    });
    await ctx.scheduler.runAfter(0, internal.orders.fetchPrice, { orderId });
    return orderId;
  },
});

// Action处理外部API调用
export const fetchPrice = internalAction({
  args: { orderId: v.id("orders") },
  returns: v.null(),
  handler: async (ctx, args) => {
    const order = await ctx.runQuery(internal.orders.getById, {
      orderId: args.orderId,
    });
    const price = await fetch(
      "https://api.stripe.com/prices/" + order.productId
    );
    await ctx.runMutation(internal.orders.updatePrice, {
      orderId: args.orderId,
      price: await price.json(),
    });
    return null;
  },
});

Pitfall 2: Accessing ctx.db in Actions

陷阱2:在Action中访问ctx.db

❌ WRONG:
typescript
export const processData = action({
  args: { id: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    // ❌ Actions don't have ctx.db!
    const data = await ctx.db.get(args.id);
    return null;
  },
});
✅ CORRECT:
typescript
export const processData = action({
  args: { id: v.id("data") },
  returns: v.null(),
  handler: async (ctx, args) => {
    // ✅ Use ctx.runQuery to read data
    const data = await ctx.runQuery(internal.data.getById, { id: args.id });

    // Do external work...
    const result = await fetch("...");

    // ✅ Use ctx.runMutation to write data
    await ctx.runMutation(internal.data.update, {
      id: args.id,
      result: await result.json(),
    });

    return null;
  },
});
❌ 错误示例:
typescript
export const processData = action({
  args: { id: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    // ❌ Action无法访问ctx.db!
    const data = await ctx.db.get(args.id);
    return null;
  },
});
✅ 正确示例:
typescript
export const processData = action({
  args: { id: v.id("data") },
  returns: v.null(),
  handler: async (ctx, args) => {
    // ✅ 使用ctx.runQuery读取数据
    const data = await ctx.runQuery(internal.data.getById, { id: args.id });

    // 执行外部操作...
    const result = await fetch("...");

    // ✅ 使用ctx.runMutation写入数据
    await ctx.runMutation(internal.data.update, {
      id: args.id,
      result: await result.json(),
    });

    return null;
  },
});

Pitfall 3: Missing returns Validator

陷阱3:缺少返回值验证器

❌ WRONG:
typescript
export const foo = mutation({
  args: {},
  handler: async (ctx) => {
    // implicitly returns undefined
  },
});
✅ CORRECT:
typescript
export const foo = mutation({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    return null;
  },
});
❌ 错误示例:
typescript
export const foo = mutation({
  args: {},
  handler: async (ctx) => {
    // 隐式返回undefined
  },
});
✅ 正确示例:
typescript
export const foo = mutation({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    return null;
  },
});

Decision Guide

决策指南

  1. Does this function read from DB? → Use
    query
    or
    internalQuery
  2. Does this function write to DB? → Use
    mutation
    or
    internalMutation
  3. Does this function call external APIs? → Use
    action
    or
    internalAction
  4. Is this called by clients? → Use public variants (
    query
    ,
    mutation
    ,
    action
    )
  5. Is this internal only? → Use
    internal*
    variants
  6. Is this a webhook endpoint? → Use
    httpAction
    in
    convex/http.ts
  1. 该函数是否需要读取数据库? → 使用
    query
    internalQuery
  2. 该函数是否需要写入数据库? → 使用
    mutation
    internalMutation
  3. 该函数是否需要调用外部API? → 使用
    action
    internalAction
  4. 该函数是否供客户端调用? → 使用公共组件(
    query
    ,
    mutation
    ,
    action
  5. 该函数是否仅内部使用? → 使用
    internal*
    系列组件
  6. 该函数是否为Webhook端点? → 在
    convex/http.ts
    中使用
    httpAction