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 需要访问该组件,请在应用中创建包装函数,而不是直接暴露组件函数。
- 运行 ,并在完成前修复 codegen、类型或边界问题。
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_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
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_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.tscomponents.notifications.lib.sendCritical Rules
关键规则
- Keep authentication in the app, because is not available inside components.
ctx.auth - Keep environment access in the app, because component functions cannot read
.
process.env - Pass parent app IDs across the boundary as strings, because types become plain strings in the app-facing
Id.ComponentApi - Do not use for app-owned tables inside component args or schema, because the component has no access to the app's table namespace.
v.id("parentTable") - Import ,
query, andmutationfrom the component's ownaction, not the app's generated files../_generated/server - 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
, because components cannot register their own HTTP routes.
convex/http.ts - If the component needs pagination, use from
paginatorinstead of built-inconvex-helpers, because.paginate()does not work across the component boundary..paginate() - Define indexes for queried fields instead of using Convex after a database query.
.filter() - Add and
argsvalidators to all public component functions, because the component boundary requires explicit type contracts.returns
- 将身份验证保留在应用中,因为组件内部无法使用 。
ctx.auth - 将环境变量访问保留在应用中,因为组件函数无法读取 。
process.env - 跨边界传递父应用 ID 时使用字符串,因为 类型在面向应用的
Id中会变为普通字符串。ComponentApi - 不要在组件参数或 schema 中对应用自有表使用 ,因为组件无法访问应用的表命名空间。
v.id("parentTable") - 从组件自己的 导入
./_generated/server、query和mutation,而不是应用的生成文件。action - 不要直接向客户端暴露组件函数。需要客户端访问时,请创建应用包装函数,因为组件属于内部实现,需要由应用提供身份验证和环境变量配置。
- 如果组件定义了 HTTP handlers,请在应用的 中挂载路由,因为组件无法注册自己的 HTTP 路由。
convex/http.ts - 如果组件需要分页,请使用 中的
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.mdValidation
验证
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 - 在 codegen 运行之前,组件本地的 导入项和应用侧的
./_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 - 已运行 并修复 codegen 或类型问题
npx convex dev