convex-create-component

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Convex Create Component

Convex 组件创建指南

Create reusable Convex components with clear boundaries and a small app-facing API.
创建具有清晰边界和轻量面向应用API的可复用Convex组件。

When to Use

适用场景

  • Creating a new Convex component in an existing app
  • Extracting reusable backend logic into a component
  • Building a third-party integration that should own its own tables and workflows
  • Packaging Convex functionality for reuse across multiple apps
  • 在现有应用中创建新的Convex组件
  • 将可复用的后端逻辑提取到组件中
  • 构建应独立拥有自身表和工作流的第三方集成
  • 打包Convex功能以在多个应用中复用

When Not to Use

不适用场景

  • One-off business logic that belongs in the main app
  • Thin utilities that do not need Convex tables or functions
  • App-level orchestration that should stay in
    convex/
  • Cases where a normal TypeScript library is enough
  • 属于主应用的一次性业务逻辑
  • 无需Convex表或函数的轻量工具类
  • 应保留在
    convex/
    目录下的应用级编排逻辑
  • 普通TypeScript库即可满足需求的场景

Workflow

工作流程

  1. Ask the user what they are building and what the end goal is. If the repo already makes the answer obvious, say so and confirm before proceeding.
  2. Choose the shape using the decision tree below and read the matching reference file.
  3. Decide whether a component is justified. Prefer normal app code or a regular library if the feature does not need isolated tables, backend functions, or reusable persistent state.
  4. Make a short plan for:
    • what tables the component owns
    • what public functions it exposes
    • what data must be passed in from the app (auth, env vars, parent IDs)
    • what stays in the app as wrappers or HTTP mounts
  5. Create the component structure with
    convex.config.ts
    ,
    schema.ts
    , and function files.
  6. Implement functions using the component's own
    ./_generated/server
    imports, not the app's generated files.
  7. Wire the component into the app with
    app.use(...)
    . If the app does not already have
    convex/convex.config.ts
    , create it.
  8. Call the component from the app through
    components.<name>
    using
    ctx.runQuery
    ,
    ctx.runMutation
    , or
    ctx.runAction
    .
  9. If React clients, HTTP callers, or public APIs need access, create wrapper functions in the app instead of exposing component functions directly.
  10. Run
    npx convex dev
    and fix codegen, type, or boundary issues before finishing.
  1. 询问用户要构建的内容和最终目标。如果仓库已能明确体现目标,需先确认后再继续。
  2. 根据下方决策树选择组件形态,并阅读对应的参考文件。
  3. 判断是否有必要使用组件。如果功能不需要隔离表、后端函数或可复用持久化状态,优先使用普通应用代码或常规库。
  4. 制定简短计划,包含:
    • 组件将拥有的表
    • 组件暴露的公共函数
    • 必须从应用传入的数据(认证信息、环境变量、父级ID)
    • 作为包装器或HTTP挂载保留在应用中的内容
  5. 创建包含
    convex.config.ts
    schema.ts
    和函数文件的组件结构。
  6. 使用组件自身的
    ./_generated/server
    导入实现函数,而非应用的生成文件。
  7. 通过
    app.use(...)
    将组件接入应用。如果应用尚未有
    convex/convex.config.ts
    ,则创建该文件。
  8. 在应用中通过
    components.<name>
    ,使用
    ctx.runQuery
    ctx.runMutation
    ctx.runAction
    调用组件。
  9. 如果React客户端、HTTP调用方或公共API需要访问组件功能,需在应用中创建包装函数,而非直接暴露组件函数。
  10. 运行
    npx convex dev
    ,在完成前修复代码生成、类型或边界相关问题。

Choose the Shape

选择组件形态

Ask the user, then pick one path:
GoalShapeReference
Component for this app onlyLocal
references/local-components.md
Publish or share across appsPackaged
references/packaged-components.md
User explicitly needs local + shared library codeHybrid
references/hybrid-components.md
Not sureDefault to local
references/local-components.md
Read exactly one reference file before proceeding.
询问用户后,选择以下其中一种方案:
目标形态参考文件
仅适用于当前应用的组件Local
references/local-components.md
需发布或跨应用共享的组件Packaged
references/packaged-components.md
用户明确需要本地+共享库代码的场景Hybrid
references/hybrid-components.md
不确定的场景默认使用Local
references/local-components.md
继续操作前需准确阅读一份参考文件。

Default Approach

默认方案

Unless the user explicitly wants an npm package, default to a local component:
  • Put it under
    convex/components/<componentName>/
  • Define it with
    defineComponent(...)
    in its own
    convex.config.ts
  • Install it from the app's
    convex/convex.config.ts
    with
    app.use(...)
  • Let
    npx convex dev
    generate the component's own
    _generated/
    files
除非用户明确需要npm包,否则默认使用本地组件:
  • 将组件放在
    convex/components/<componentName>/
    目录下
  • 在组件自身的
    convex.config.ts
    中使用
    defineComponent(...)
    定义组件
  • 在应用的
    convex/convex.config.ts
    中通过
    app.use(...)
    安装组件
  • npx convex dev
    生成组件自身的
    _generated/
    文件

Component Skeleton

组件骨架

A minimal local component with a table and two functions, plus the app wiring.
ts
// convex/components/notifications/convex.config.ts
import { defineComponent } from "convex/server";

export default defineComponent("notifications");
ts
// convex/components/notifications/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  notifications: defineTable({
    userId: v.string(),
    message: v.string(),
    read: v.boolean(),
  }).index("by_user", ["userId"]),
});
ts
// convex/components/notifications/lib.ts
import { v } from "convex/values";
import { mutation, query } from "./_generated/server.js";

export const send = mutation({
  args: { userId: v.string(), message: v.string() },
  returns: v.id("notifications"),
  handler: async (ctx, args) => {
    return await ctx.db.insert("notifications", {
      userId: args.userId,
      message: args.message,
      read: false,
    });
  },
});

export const listUnread = query({
  args: { userId: v.string() },
  returns: v.array(
    v.object({
      _id: v.id("notifications"),
      _creationTime: v.number(),
      userId: v.string(),
      message: v.string(),
      read: v.boolean(),
    })
  ),
  handler: async (ctx, args) => {
    return await ctx.db
      .query("notifications")
      .withIndex("by_user", (q) => q.eq("userId", args.userId))
      .filter((q) => q.eq(q.field("read"), false))
      .collect();
  },
});
ts
// convex/convex.config.ts
import { defineApp } from "convex/server";
import notifications from "./components/notifications/convex.config.js";

const app = defineApp();
app.use(notifications);

export default app;
ts
// convex/notifications.ts  (app-side wrapper)
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { components } from "./_generated/api";
import { getAuthUserId } from "@convex-dev/auth/server";

export const sendNotification = mutation({
  args: { message: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new Error("Not authenticated");

    await ctx.runMutation(components.notifications.lib.send, {
      userId,
      message: args.message,
    });
    return null;
  },
});

export const myUnread = query({
  args: {},
  handler: async (ctx) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new Error("Not authenticated");

    return await ctx.runQuery(components.notifications.lib.listUnread, {
      userId,
    });
  },
});
Note the reference path shape: a function in
convex/components/notifications/lib.ts
is called as
components.notifications.lib.send
from the app.
一个包含表和两个函数的最小本地组件,以及对应的应用接入代码。
ts
// convex/components/notifications/convex.config.ts
import { defineComponent } from "convex/server";

export default defineComponent("notifications");
ts
// convex/components/notifications/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  notifications: defineTable({
    userId: v.string(),
    message: v.string(),
    read: v.boolean(),
  }).index("by_user", ["userId"]),
});
ts
// convex/components/notifications/lib.ts
import { v } from "convex/values";
import { mutation, query } from "./_generated/server.js";

export const send = mutation({
  args: { userId: v.string(), message: v.string() },
  returns: v.id("notifications"),
  handler: async (ctx, args) => {
    return await ctx.db.insert("notifications", {
      userId: args.userId,
      message: args.message,
      read: false,
    });
  },
});

export const listUnread = query({
  args: { userId: v.string() },
  returns: v.array(
    v.object({
      _id: v.id("notifications"),
      _creationTime: v.number(),
      userId: v.string(),
      message: v.string(),
      read: v.boolean(),
    })
  ),
  handler: async (ctx, args) => {
    return await ctx.db
      .query("notifications")
      .withIndex("by_user", (q) => q.eq("userId", args.userId))
      .filter((q) => q.eq(q.field("read"), false))
      .collect();
  },
});
ts
// convex/convex.config.ts
import { defineApp } from "convex/server";
import notifications from "./components/notifications/convex.config.js";

const app = defineApp();
app.use(notifications);

export default app;
ts
// convex/notifications.ts  (应用侧包装器)
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { components } from "./_generated/api";
import { getAuthUserId } from "@convex-dev/auth/server";

export const sendNotification = mutation({
  args: { message: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new Error("未认证");

    await ctx.runMutation(components.notifications.lib.send, {
      userId,
      message: args.message,
    });
    return null;
  },
});

export const myUnread = query({
  args: {},
  handler: async (ctx) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new Error("未认证");

    return await ctx.runQuery(components.notifications.lib.listUnread, {
      userId,
    });
  },
});
注意引用路径格式:
convex/components/notifications/lib.ts
中的函数在应用中通过
components.notifications.lib.send
调用。

Critical Rules

核心规则

  • Keep authentication in the app.
    ctx.auth
    is not available inside components.
  • Keep environment access in the app. Component functions cannot read
    process.env
    .
  • Pass parent app IDs across the boundary as strings.
    Id
    types become plain strings in the app-facing
    ComponentApi
    .
  • Do not use
    v.id("parentTable")
    for app-owned tables inside component args or schema.
  • Import
    query
    ,
    mutation
    , and
    action
    from the component's own
    ./_generated/server
    .
  • Do not expose component functions directly to clients. Create app wrappers when client access is needed.
  • If the component defines HTTP handlers, mount the routes in the app's
    convex/http.ts
    .
  • If the component needs pagination, use
    paginator
    from
    convex-helpers
    instead of built-in
    .paginate()
    .
  • Add
    args
    and
    returns
    validators to all public component functions.
  • 认证逻辑保留在应用中。组件内部无法访问
    ctx.auth
  • 环境变量访问保留在应用中。组件函数无法读取
    process.env
  • 将父应用的ID以字符串形式传递到组件边界。在面向应用的
    ComponentApi
    中,
    Id
    类型会转换为普通字符串。
  • 组件的参数或Schema中不要使用
    v.id("parentTable")
    来引用应用拥有的表。
  • 从组件自身的
    ./_generated/server
    导入
    query
    mutation
    action
  • 不要直接向客户端暴露组件函数。需要客户端访问时,在应用中创建包装函数。
  • 如果组件定义了HTTP处理器,需在应用的
    convex/http.ts
    中挂载路由。
  • 如果组件需要分页,使用
    convex-helpers
    中的
    paginator
    而非内置的
    .paginate()
  • 为所有公共组件函数添加
    args
    returns
    验证器。

Patterns

设计模式

Authentication and environment access

认证与环境变量访问

ts
// Bad: component code cannot rely on app auth or env
const identity = await ctx.auth.getUserIdentity();
const apiKey = process.env.OPENAI_API_KEY;
ts
// Good: the app resolves auth and env, then passes explicit values
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error("Not authenticated");

await ctx.runAction(components.translator.translate, {
  userId,
  apiKey: process.env.OPENAI_API_KEY,
  text: args.text,
});
ts
// 错误:组件代码不能依赖应用的认证或环境变量
const identity = await ctx.auth.getUserIdentity();
const apiKey = process.env.OPENAI_API_KEY;
ts
// 正确:应用解析认证和环境变量,然后传递明确的值
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error("未认证");

await ctx.runAction(components.translator.translate, {
  userId,
  apiKey: process.env.OPENAI_API_KEY,
  text: args.text,
});

Client-facing API

面向客户端的API

ts
// Bad: assuming a component function is directly callable by clients
export const send = components.notifications.send;
ts
// Good: re-export through an app mutation or query
export const sendNotification = mutation({
  args: { message: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new Error("Not authenticated");

    await ctx.runMutation(components.notifications.lib.send, {
      userId,
      message: args.message,
    });
    return null;
  },
});
ts
// 错误:假设组件函数可直接被客户端调用
export const send = components.notifications.send;
ts
// 正确:通过应用的mutation或query重新导出
export const sendNotification = mutation({
  args: { message: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new Error("未认证");

    await ctx.runMutation(components.notifications.lib.send, {
      userId,
      message: args.message,
    });
    return null;
  },
});

IDs across the boundary

跨边界的ID处理

ts
// Bad: parent app table IDs are not valid component validators
args: { userId: v.id("users") }
ts
// Good: treat parent-owned IDs as strings at the boundary
args: { userId: v.string() }
ts
// 错误:父应用表的ID不能作为组件的有效验证器
args: { userId: v.id("users") }
ts
// 正确:在边界处将父应用的ID视为字符串
args: { userId: v.string() }

Function Handles for callbacks

用于回调的函数句柄

When the app needs to pass a callback function to the component, use function handles. This is common for components that run app-defined logic on a schedule or in a workflow.
ts
// App side: create a handle and pass it to the component
import { createFunctionHandle } from "convex/server";

export const startJob = mutation({
  handler: async (ctx) => {
    const handle = await createFunctionHandle(internal.myModule.processItem);
    await ctx.runMutation(components.workpool.enqueue, {
      callback: handle,
    });
  },
});
ts
// Component side: accept and invoke the handle
import { v } from "convex/values";
import type { FunctionHandle } from "convex/server";
import { mutation } from "./_generated/server.js";

export const enqueue = mutation({
  args: { callback: v.string() },
  handler: async (ctx, args) => {
    const handle = args.callback as FunctionHandle<"mutation">;
    await ctx.scheduler.runAfter(0, handle, {});
  },
});
当应用需要向组件传递回调函数时,使用函数句柄。这种模式常见于需要按计划或在工作流中运行应用定义逻辑的组件。
ts
// 应用侧:创建句柄并传递给组件
import { createFunctionHandle } from "convex/server";

export const startJob = mutation({
  handler: async (ctx) => {
    const handle = await createFunctionHandle(internal.myModule.processItem);
    await ctx.runMutation(components.workpool.enqueue, {
      callback: handle,
    });
  },
});
ts
// 组件侧:接收并调用句柄
import { v } from "convex/values";
import type { FunctionHandle } from "convex/server";
import { mutation } from "./_generated/server.js";

export const enqueue = mutation({
  args: { callback: v.string() },
  handler: async (ctx, args) => {
    const handle = args.callback as FunctionHandle<"mutation">;
    await ctx.scheduler.runAfter(0, handle, {});
  },
});

Deriving validators from schema

从Schema派生验证器

Instead of manually repeating field types in return validators, extend the schema validator:
ts
import { v } from "convex/values";
import schema from "./schema.js";

const notificationDoc = schema.tables.notifications.validator.extend({
  _id: v.id("notifications"),
  _creationTime: v.number(),
});

export const getLatest = query({
  args: {},
  returns: v.nullable(notificationDoc),
  handler: async (ctx) => {
    return await ctx.db.query("notifications").order("desc").first();
  },
});
无需在返回验证器中手动重复字段类型,可扩展Schema验证器:
ts
import { v } from "convex/values";
import schema from "./schema.js";

const notificationDoc = schema.tables.notifications.validator.extend({
  _id: v.id("notifications"),
  _creationTime: v.number(),
});

export const getLatest = query({
  args: {},
  returns: v.nullable(notificationDoc),
  handler: async (ctx) => {
    return await ctx.db.query("notifications").order("desc").first();
  },
});

Static configuration with a globals table

使用全局表进行静态配置

A common pattern for component configuration is a single-document "globals" table:
ts
// schema.ts
export default defineSchema({
  globals: defineTable({
    maxRetries: v.number(),
    webhookUrl: v.optional(v.string()),
  }),
  // ... other tables
});
ts
// lib.ts
export const configure = mutation({
  args: { maxRetries: v.number(), webhookUrl: v.optional(v.string()) },
  returns: v.null(),
  handler: async (ctx, args) => {
    const existing = await ctx.db.query("globals").first();
    if (existing) {
      await ctx.db.patch(existing._id, args);
    } else {
      await ctx.db.insert("globals", args);
    }
    return null;
  },
});
组件配置的常见模式是使用单文档的“globals”表:
ts
// schema.ts
export default defineSchema({
  globals: defineTable({
    maxRetries: v.number(),
    webhookUrl: v.optional(v.string()),
  }),
  // ... 其他表
});
ts
// lib.ts
export const configure = mutation({
  args: { maxRetries: v.number(), webhookUrl: v.optional(v.string()) },
  returns: v.null(),
  handler: async (ctx, args) => {
    const existing = await ctx.db.query("globals").first();
    if (existing) {
      await ctx.db.patch(existing._id, args);
    } else {
      await ctx.db.insert("globals", args);
    }
    return null;
  },
});

Class-based client wrappers

基于类的客户端包装器

For components with many functions or configuration options, a class-based client provides a cleaner API. This pattern is common in published components.
ts
// src/client/index.ts
import type { GenericMutationCtx, GenericDataModel } from "convex/server";
import type { ComponentApi } from "../component/_generated/component.js";

type MutationCtx = Pick<GenericMutationCtx<GenericDataModel>, "runMutation">;

export class Notifications {
  constructor(
    private component: ComponentApi,
    private options?: { defaultChannel?: string },
  ) {}

  async send(ctx: MutationCtx, args: { userId: string; message: string }) {
    return await ctx.runMutation(this.component.lib.send, {
      ...args,
      channel: this.options?.defaultChannel ?? "default",
    });
  }
}
ts
// App usage
import { Notifications } from "@convex-dev/notifications";
import { components } from "./_generated/api";

const notifications = new Notifications(components.notifications, {
  defaultChannel: "alerts",
});

export const send = mutation({
  args: { message: v.string() },
  handler: async (ctx, args) => {
    const userId = await getAuthUserId(ctx);
    await notifications.send(ctx, { userId, message: args.message });
  },
});
对于包含多个函数或配置选项的组件,基于类的客户端能提供更简洁的API。这种模式在已发布的组件中很常见。
ts
// src/client/index.ts
import type { GenericMutationCtx, GenericDataModel } from "convex/server";
import type { ComponentApi } from "../component/_generated/component.js";

type MutationCtx = Pick<GenericMutationCtx<GenericDataModel>, "runMutation">;

export class Notifications {
  constructor(
    private component: ComponentApi,
    private options?: { defaultChannel?: string },
  ) {}

  async send(ctx: MutationCtx, args: { userId: string; message: string }) {
    return await ctx.runMutation(this.component.lib.send, {
      ...args,
      channel: this.options?.defaultChannel ?? "default",
    });
  }
}
ts
// 应用使用示例
import { Notifications } from "@convex-dev/notifications";
import { components } from "./_generated/api";

const notifications = new Notifications(components.notifications, {
  defaultChannel: "alerts",
});

export const send = mutation({
  args: { message: v.string() },
  handler: async (ctx, args) => {
    const userId = await getAuthUserId(ctx);
    await notifications.send(ctx, { userId, message: args.message });
  },
});

Validation

验证步骤

Try validation in this order:
  1. npx convex codegen --component-dir convex/components/<name>
  2. npx convex codegen
  3. npx convex dev
Important:
  • Fresh repos may fail these commands until
    CONVEX_DEPLOYMENT
    is configured.
  • Until codegen runs, component-local
    ./_generated/*
    imports and app-side
    components.<name>...
    references will not typecheck.
  • If validation blocks on Convex login or deployment setup, stop and ask the user for that exact step instead of guessing.
按以下顺序进行验证:
  1. npx convex codegen --component-dir convex/components/<name>
  2. npx convex codegen
  3. npx convex dev
注意事项:
  • 新仓库在配置
    CONVEX_DEPLOYMENT
    前可能会执行这些命令失败。
  • 在代码生成完成前,组件本地的
    ./_generated/*
    导入和应用侧的
    components.<name>...
    引用无法通过类型检查。
  • 如果验证过程卡在Convex登录或部署设置步骤,请停止操作并询问用户完成该步骤,不要自行猜测。

Reference Files

参考文件

Read exactly one of these after the user confirms the goal:
  • references/local-components.md
  • references/packaged-components.md
  • references/hybrid-components.md
Official docs: Authoring Components
用户确认目标后,需阅读以下其中一份文件:
  • references/local-components.md
  • references/packaged-components.md
  • references/hybrid-components.md
官方文档:组件编写指南

Checklist

检查清单

  • Asked the user what they want to build and confirmed the shape
  • Read the matching reference file
  • Confirmed a component is the right abstraction
  • Planned tables, public API, boundaries, and app wrappers
  • Component lives under
    convex/components/<name>/
    (or package layout if publishing)
  • Component imports from its own
    ./_generated/server
  • Auth, env access, and HTTP routes stay in the app
  • Parent app IDs cross the boundary as
    v.string()
  • Public functions have
    args
    and
    returns
    validators
  • Ran
    npx convex dev
    and fixed codegen or type issues
  • 询问用户要构建的内容并确认组件形态
  • 阅读了对应的参考文件
  • 确认组件是合适的抽象方案
  • 规划了表、公共API、边界和应用包装器
  • 组件位于
    convex/components/<name>/
    目录下(如果是发布组件则使用包布局)
  • 组件从自身的
    ./_generated/server
    导入依赖
  • 认证、环境变量访问和HTTP路由保留在应用中
  • 父应用的ID以
    v.string()
    形式跨边界传递
  • 公共函数添加了
    args
    returns
    验证器
  • 运行
    npx convex dev
    并修复了代码生成或类型问题