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
    ,并在完成前修复 codegen、类型或边界问题。

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_read", ["userId", "read"]),
});
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_read", (q) =>
        q.eq("userId", args.userId).eq("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_read", ["userId", "read"]),
});
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_read", (q) =>
        q.eq("userId", args.userId).eq("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,
    });
  },
});
请注意引用路径结构:
convex/components/notifications/lib.ts
中的函数在应用中通过
components.notifications.lib.send
调用。

Critical Rules

关键规则

  • Keep authentication in the app, because
    ctx.auth
    is not available inside components.
  • Keep environment access in the app, because component functions cannot read
    process.env
    .
  • Pass parent app IDs across the boundary as strings, because
    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, because the component has no access to the app's table namespace.
  • Import
    query
    ,
    mutation
    , and
    action
    from the component's own
    ./_generated/server
    , not the app's generated files.
  • Do not expose component functions directly to clients. Create app wrappers when client access is needed, because components are internal and need auth/env wiring the app provides.
  • If the component defines HTTP handlers, mount the routes in the app's
    convex/http.ts
    , because components cannot register their own HTTP routes.
  • If the component needs pagination, use
    paginator
    from
    convex-helpers
    instead of built-in
    .paginate()
    , because
    .paginate()
    does not work across the component boundary.
  • Define indexes for queried fields instead of using Convex
    .filter()
    after a database query.
  • Add
    args
    and
    returns
    validators to all public component functions, because the component boundary requires explicit type contracts.
  • 将身份验证保留在应用中,因为组件内部无法使用
    ctx.auth
    。
  • 将环境变量访问保留在应用中,因为组件函数无法读取
    process.env
    。
  • 跨边界传递父应用 ID 时使用字符串,因为
    Id
    类型在面向应用的
    ComponentApi
    中会变为普通字符串。
  • 不要在组件参数或 schema 中对应用自有表使用
    v.id("parentTable")
    ,因为组件无法访问应用的表命名空间。
  • 从组件自己的
    ./_generated/server
    导入
    query
    、
    mutation
    和
    action
    ,而不是应用的生成文件。
  • 不要直接向客户端暴露组件函数。需要客户端访问时,请创建应用包装函数,因为组件属于内部实现,需要由应用提供身份验证和环境变量配置。
  • 如果组件定义了 HTTP handlers,请在应用的
    convex/http.ts
    中挂载路由,因为组件无法注册自己的 HTTP 路由。
  • 如果组件需要分页,请使用
    convex-helpers
    中的
    paginator
    ,而不是内置的
    .paginate()
    ,因为
    .paginate()
    无法跨组件边界工作。
  • 为查询字段定义索引,而不是在数据库查询后使用 Convex
    .filter()
    。
  • 为所有公共组件函数添加
    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
// 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,
});

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
// 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;
  },
});

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
// 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(),
}

Advanced Patterns

高级模式

For additional patterns including function handles for callbacks, deriving validators from schema, static configuration with a globals table, and class-based client wrappers, see
references/advanced-patterns.md
.
有关更多模式,包括用于回调的函数句柄、从 schema 派生验证器、使用 globals 表进行静态配置,以及基于类的客户端包装器,请参阅
references/advanced-patterns.md
。

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
    之前,这些命令可能会失败。
  • 在 codegen 运行之前,组件本地的
    ./_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
    并修复 codegen 或类型问题