convex-create-component
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseConvex 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
工作流程
- 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.
- Choose the shape using the decision tree below and read the matching reference file.
- 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.
- 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
- Create the component structure with ,
convex.config.ts, and function files.schema.ts - Implement functions using the component's own imports, not the app's generated files.
./_generated/server - Wire the component into the app with . If the app does not already have
app.use(...), create it.convex/convex.config.ts - Call the component from the app through using
components.<name>,ctx.runQuery, orctx.runMutation.ctx.runAction - If React clients, HTTP callers, or public APIs need access, create wrapper functions in the app instead of exposing component functions directly.
- Run and fix codegen, type, or boundary issues before finishing.
npx convex dev
- 询问用户要构建的内容和最终目标。如果仓库已能明确体现目标,需先确认后再继续。
- 根据下方决策树选择组件形态,并阅读对应的参考文件。
- 判断是否有必要使用组件。如果功能不需要隔离表、后端函数或可复用持久化状态,优先使用普通应用代码或常规库。
- 制定简短计划,包含:
- 组件将拥有的表
- 组件暴露的公共函数
- 必须从应用传入的数据(认证信息、环境变量、父级ID)
- 作为包装器或HTTP挂载保留在应用中的内容
- 创建包含、
convex.config.ts和函数文件的组件结构。schema.ts - 使用组件自身的导入实现函数,而非应用的生成文件。
./_generated/server - 通过将组件接入应用。如果应用尚未有
app.use(...),则创建该文件。convex/convex.config.ts - 在应用中通过,使用
components.<name>、ctx.runQuery或ctx.runMutation调用组件。ctx.runAction - 如果React客户端、HTTP调用方或公共API需要访问组件功能,需在应用中创建包装函数,而非直接暴露组件函数。
- 运行,在完成前修复代码生成、类型或边界相关问题。
npx convex dev
Choose the Shape
选择组件形态
Ask the user, then pick one path:
| Goal | Shape | Reference |
|---|---|---|
| Component for this app only | Local | |
| Publish or share across apps | Packaged | |
| User explicitly needs local + shared library code | Hybrid | |
| Not sure | Default to local | |
Read exactly one reference file before proceeding.
询问用户后,选择以下其中一种方案:
| 目标 | 形态 | 参考文件 |
|---|---|---|
| 仅适用于当前应用的组件 | Local | |
| 需发布或跨应用共享的组件 | Packaged | |
| 用户明确需要本地+共享库代码的场景 | Hybrid | |
| 不确定的场景 | 默认使用Local | |
继续操作前需准确阅读一份参考文件。
Default Approach
默认方案
Unless the user explicitly wants an npm package, default to a local component:
- Put it under
convex/components/<componentName>/ - Define it with in its own
defineComponent(...)convex.config.ts - Install it from the app's with
convex/convex.config.tsapp.use(...) - Let generate the component's own
npx convex devfiles_generated/
除非用户明确需要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 is called as from the app.
convex/components/notifications/lib.tscomponents.notifications.lib.send一个包含表和两个函数的最小本地组件,以及对应的应用接入代码。
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.tscomponents.notifications.lib.sendCritical Rules
核心规则
- Keep authentication in the app. is not available inside components.
ctx.auth - Keep environment access in the app. Component functions cannot read .
process.env - Pass parent app IDs across the boundary as strings. types become plain strings in the app-facing
Id.ComponentApi - Do not use for app-owned tables inside component args or schema.
v.id("parentTable") - Import ,
query, andmutationfrom the component's ownaction../_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 from
paginatorinstead of built-inconvex-helpers..paginate() - Add and
argsvalidators to all public component functions.returns
- 认证逻辑保留在应用中。组件内部无法访问。
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:
npx convex codegen --component-dir convex/components/<name>npx convex codegennpx convex dev
Important:
- Fresh repos may fail these commands until is configured.
CONVEX_DEPLOYMENT - Until codegen runs, component-local imports and app-side
./_generated/*references will not typecheck.components.<name>... - If validation blocks on Convex login or deployment setup, stop and ask the user for that exact step instead of guessing.
按以下顺序进行验证:
npx convex codegen --component-dir convex/components/<name>npx convex codegennpx convex dev
注意事项:
- 新仓库在配置前可能会执行这些命令失败。
CONVEX_DEPLOYMENT - 在代码生成完成前,组件本地的导入和应用侧的
./_generated/*引用无法通过类型检查。components.<name>... - 如果验证过程卡在Convex登录或部署设置步骤,请停止操作并询问用户完成该步骤,不要自行猜测。
Reference Files
参考文件
Read exactly one of these after the user confirms the goal:
references/local-components.mdreferences/packaged-components.mdreferences/hybrid-components.md
Official docs: Authoring Components
用户确认目标后,需阅读以下其中一份文件:
references/local-components.mdreferences/packaged-components.mdreferences/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 (or package layout if publishing)
convex/components/<name>/ - 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 and
argsvalidatorsreturns - Ran and fixed codegen or type issues
npx convex dev
- 询问用户要构建的内容并确认组件形态
- 阅读了对应的参考文件
- 确认组件是合适的抽象方案
- 规划了表、公共API、边界和应用包装器
- 组件位于目录下(如果是发布组件则使用包布局)
convex/components/<name>/ - 组件从自身的导入依赖
./_generated/server - 认证、环境变量访问和HTTP路由保留在应用中
- 父应用的ID以形式跨边界传递
v.string() - 公共函数添加了和
args验证器returns - 运行并修复了代码生成或类型问题
npx convex dev