components-guide by get-convex/convex-agent-plugins
npx skills add https://github.com/get-convex/convex-agent-plugins --skill components-guide使用组件来封装功能并构建可维护、可复用的后端系统。
组件是自包含的微型后端,它们捆绑了:
可以将它们视为: 你后端的 npm 包,或者无需复杂部署的微服务。
convex/
├── users.ts (500 行)
├── files.ts (600 行 - 上传、存储、权限、速率限制)
├── payments.ts (400 行 - Stripe、Webhooks、计费)
├── notifications.ts (300 行)
└── analytics.ts (200 行)
总计:一个庞大的代码库,所有内容混杂在一起
convex/
├── components/
│ ├── storage/ (文件上传 - 可复用)
│ ├── billing/ (支付 - 可复用)
│ ├── notifications/ (通知 - 可复用)
│ └── analytics/ (分析 - 可复用)
├── convex.config.ts (连接组件)
└── domain/ (你的实际业务逻辑)
├── users.ts (50 行 - 使用组件)
└── projects.ts (75 行 - 使用组件)
总计:清晰、专注、可复用
# 来自 npm 的官方组件
npm install @convex-dev/ratelimiter
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
import { defineApp } from "convex/server";
import ratelimiter from "@convex-dev/ratelimiter/convex.config";
export default defineApp({
components: {
ratelimiter,
},
});
import { components } from "./_generated/api";
export const createPost = mutation({
handler: async (ctx, args) => {
// 使用组件
await components.ratelimiter.check(ctx, {
key: `user:${ctx.user._id}`,
limit: 10,
period: 60000, // 每分钟 10 次请求
});
return await ctx.db.insert("posts", args);
},
});
多个组件在同一层级协同工作:
// convex.config.ts
export default defineApp({
components: {
// 同级组件 - 每个处理一个关注点
auth: authComponent,
storage: storageComponent,
payments: paymentsComponent,
emails: emailComponent,
analytics: analyticsComponent,
},
});
// convex/subscriptions.ts
import { components } from "./_generated/api";
export const subscribe = mutation({
args: { plan: v.string() },
handler: async (ctx, args) => {
// 1. 验证身份验证(auth 组件)
const user = await components.auth.getCurrentUser(ctx);
// 2. 创建支付(payments 组件)
const subscription = await components.payments.createSubscription(ctx, {
userId: user._id,
plan: args.plan,
amount: getPlanAmount(args.plan),
});
// 3. 跟踪转化(analytics 组件)
await components.analytics.track(ctx, {
event: "subscription_created",
userId: user._id,
plan: args.plan,
});
// 4. 发送确认邮件(emails 组件)
await components.emails.send(ctx, {
to: user.email,
template: "subscription_welcome",
data: { plan: args.plan },
});
// 5. 在主应用中存储订阅信息
await ctx.db.insert("subscriptions", {
userId: user._id,
paymentId: subscription.id,
plan: args.plan,
status: "active",
});
return subscription;
},
});
这实现了什么:
浏览 组件目录:
好的理由:
不好的理由:
mkdir -p convex/components/notifications
// convex/components/notifications/convex.config.ts
import { defineComponent } from "convex/server";
export default defineComponent("notifications");
// convex/components/notifications/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
notifications: defineTable({
userId: v.id("users"),
message: v.string(),
read: v.boolean(),
createdAt: v.number(),
})
.index("by_user", ["userId"])
.index("by_user_and_read", ["userId", "read"]),
});
// convex/components/notifications/send.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const send = mutation({
args: {
userId: v.id("users"),
message: v.string(),
},
handler: async (ctx, args) => {
await ctx.db.insert("notifications", {
userId: args.userId,
message: args.message,
read: false,
createdAt: Date.now(),
});
},
});
export const markRead = mutation({
args: { notificationId: v.id("notifications") },
handler: async (ctx, args) => {
await ctx.db.patch(args.notificationId, { read: true });
},
});
// convex/components/notifications/read.ts
import { query } from "./_generated/server";
import { v } from "convex/values";
export const list = query({
args: { userId: v.id("users") },
handler: async (ctx, args) => {
return await ctx.db
.query("notifications")
.withIndex("by_user", q => q.eq("userId", args.userId))
.order("desc")
.collect();
},
});
export const unreadCount = query({
args: { userId: v.id("users") },
handler: async (ctx, args) => {
const unread = await ctx.db
.query("notifications")
.withIndex("by_user_and_read", q =>
q.eq("userId", args.userId).eq("read", false)
)
.collect();
return unread.length;
},
});
// convex.config.ts
import { defineApp } from "convex/server";
import notifications from "./components/notifications/convex.config";
export default defineApp({
components: {
notifications, // 你的本地组件
},
});
// convex/tasks.ts - 主应用代码
import { components } from "./_generated/api";
export const completeTask = mutation({
args: { taskId: v.id("tasks") },
handler: async (ctx, args) => {
const task = await ctx.db.get(args.taskId);
await ctx.db.patch(args.taskId, { completed: true });
// 使用你的组件
await components.notifications.send(ctx, {
userId: task.userId,
message: `任务 "${task.title}" 已完成!`,
});
},
});
// 主应用调用组件
await components.storage.upload(ctx, file);
await components.analytics.track(ctx, event);
// 主应用编排多个组件
await components.auth.verify(ctx);
const file = await components.storage.upload(ctx, data);
await components.notifications.send(ctx, message);
// 将父级表中的 ID 传递给组件
await components.audit.log(ctx, {
userId: user._id, // 来自父级的 users 表
action: "delete",
resourceId: task._id, // 来自父级的 tasks 表
});
// 组件将这些存储为字符串/ID
// 但不直接访问父级表
// 在组件代码内部 - 不要这样做
const user = await ctx.db.get(userId); // 错误!无法访问父级表
组件不能直接相互调用。如果需要此功能,它们应位于主应用中或重新设计。
// convex.config.ts
export default defineApp({
components: {
auth: "@convex-dev/better-auth",
organizations: "./components/organizations",
billing: "./components/billing",
storage: "@convex-dev/r2",
analytics: "./components/analytics",
emails: "./components/emails",
},
});
每个组件:
auth - 用户身份验证和会话organizations - 租户隔离和权限billing - Stripe 集成和订阅storage - 文件上传到 R2analytics - 事件跟踪和指标emails - 通过 SendGrid 发送邮件export default defineApp({
components: {
cart: "./components/cart",
inventory: "./components/inventory",
orders: "./components/orders",
payments: "@convex-dev/polar",
shipping: "./components/shipping",
recommendations: "./components/recommendations",
},
});
export default defineApp({
components: {
agent: "@convex-dev/agent",
embeddings: "./components/embeddings",
documents: "./components/documents",
chat: "./components/chat",
workflow: "@convex-dev/workflow",
},
});
步骤 1:识别功能
当前单体结构:
- 文件上传(与主应用混合)
- 速率限制(分散在各处)
- 分析(嵌入在函数中)
步骤 2:提取一个功能
# 创建组件
mkdir -p convex/components/storage
# 将存储代码移动到组件
# 在主应用中更新导入
步骤 3:独立测试
# 组件有自己的测试
# 不与主应用耦合
步骤 4:重复 逐步提取其他功能。
每个组件只做好一件事:
// 仅导出所需内容
export { upload, download, delete } from "./storage";
// 保持内部私有
// (不要导出辅助函数)
// ✅ 良好:将数据作为参数传递
await components.audit.log(ctx, {
userId: user._id,
action: "delete"
});
// ❌ 不良:组件访问父级表
// (即使不可能,但说明了原则)
{
"name": "@yourteam/notifications-component",
"version": "1.0.0"
}
包含 README,说明:
# 确保组件在 convex.config.ts 中
# 运行:npx convex dev
这是设计使然!组件是沙盒化的。
请将数据作为参数传递。
每个组件都有隔离的表。
组件无法看到彼此的数据。
npm install @convex-dev/component-nameconvex.config.ts 中配置记住: 组件关乎封装和可复用性。如有疑问,优先选择组件而非单体式代码!
每周安装次数
81
仓库
GitHub 星标数
90
首次出现
2026年2月6日
安全审计
安装于
claude-code65
codex65
opencode47
gemini-cli47
github-copilot47
amp46
Use components to encapsulate features and build maintainable, reusable backends.
Components are self-contained mini-backends that bundle:
Think of them as: npm packages for your backend, or microservices without the deployment complexity.
convex/
├── users.ts (500 lines)
├── files.ts (600 lines - upload, storage, permissions, rate limiting)
├── payments.ts (400 lines - Stripe, webhooks, billing)
├── notifications.ts (300 lines)
└── analytics.ts (200 lines)
Total: One big codebase, everything mixed together
convex/
├── components/
│ ├── storage/ (File uploads - reusable)
│ ├── billing/ (Payments - reusable)
│ ├── notifications/ (Alerts - reusable)
│ └── analytics/ (Tracking - reusable)
├── convex.config.ts (Wire components together)
└── domain/ (Your actual business logic)
├── users.ts (50 lines - uses components)
└── projects.ts (75 lines - uses components)
Total: Clean, focused, reusable
# Official components from npm
npm install @convex-dev/ratelimiter
import { defineApp } from "convex/server";
import ratelimiter from "@convex-dev/ratelimiter/convex.config";
export default defineApp({
components: {
ratelimiter,
},
});
import { components } from "./_generated/api";
export const createPost = mutation({
handler: async (ctx, args) => {
// Use the component
await components.ratelimiter.check(ctx, {
key: `user:${ctx.user._id}`,
limit: 10,
period: 60000, // 10 requests per minute
});
return await ctx.db.insert("posts", args);
},
});
Multiple components working together at the same level:
// convex.config.ts
export default defineApp({
components: {
// Sibling components - each handles one concern
auth: authComponent,
storage: storageComponent,
payments: paymentsComponent,
emails: emailComponent,
analytics: analyticsComponent,
},
});
// convex/subscriptions.ts
import { components } from "./_generated/api";
export const subscribe = mutation({
args: { plan: v.string() },
handler: async (ctx, args) => {
// 1. Verify authentication (auth component)
const user = await components.auth.getCurrentUser(ctx);
// 2. Create payment (payments component)
const subscription = await components.payments.createSubscription(ctx, {
userId: user._id,
plan: args.plan,
amount: getPlanAmount(args.plan),
});
// 3. Track conversion (analytics component)
await components.analytics.track(ctx, {
event: "subscription_created",
userId: user._id,
plan: args.plan,
});
// 4. Send confirmation (emails component)
await components.emails.send(ctx, {
to: user.email,
template: "subscription_welcome",
data: { plan: args.plan },
});
// 5. Store subscription in main app
await ctx.db.insert("subscriptions", {
userId: user._id,
paymentId: subscription.id,
plan: args.plan,
status: "active",
});
return subscription;
},
});
What this achieves:
Browse Component Directory:
Good reasons:
Not good reasons:
mkdir -p convex/components/notifications
// convex/components/notifications/convex.config.ts
import { defineComponent } from "convex/server";
export default defineComponent("notifications");
// convex/components/notifications/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
notifications: defineTable({
userId: v.id("users"),
message: v.string(),
read: v.boolean(),
createdAt: v.number(),
})
.index("by_user", ["userId"])
.index("by_user_and_read", ["userId", "read"]),
});
// convex/components/notifications/send.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const send = mutation({
args: {
userId: v.id("users"),
message: v.string(),
},
handler: async (ctx, args) => {
await ctx.db.insert("notifications", {
userId: args.userId,
message: args.message,
read: false,
createdAt: Date.now(),
});
},
});
export const markRead = mutation({
args: { notificationId: v.id("notifications") },
handler: async (ctx, args) => {
await ctx.db.patch(args.notificationId, { read: true });
},
});
// convex/components/notifications/read.ts
import { query } from "./_generated/server";
import { v } from "convex/values";
export const list = query({
args: { userId: v.id("users") },
handler: async (ctx, args) => {
return await ctx.db
.query("notifications")
.withIndex("by_user", q => q.eq("userId", args.userId))
.order("desc")
.collect();
},
});
export const unreadCount = query({
args: { userId: v.id("users") },
handler: async (ctx, args) => {
const unread = await ctx.db
.query("notifications")
.withIndex("by_user_and_read", q =>
q.eq("userId", args.userId).eq("read", false)
)
.collect();
return unread.length;
},
});
// convex.config.ts
import { defineApp } from "convex/server";
import notifications from "./components/notifications/convex.config";
export default defineApp({
components: {
notifications, // Your local component
},
});
// convex/tasks.ts - main app code
import { components } from "./_generated/api";
export const completeTask = mutation({
args: { taskId: v.id("tasks") },
handler: async (ctx, args) => {
const task = await ctx.db.get(args.taskId);
await ctx.db.patch(args.taskId, { completed: true });
// Use your component
await components.notifications.send(ctx, {
userId: task.userId,
message: `Task "${task.title}" completed!`,
});
},
});
// Main app calls component
await components.storage.upload(ctx, file);
await components.analytics.track(ctx, event);
// Main app orchestrates multiple components
await components.auth.verify(ctx);
const file = await components.storage.upload(ctx, data);
await components.notifications.send(ctx, message);
// Pass IDs from parent's tables to component
await components.audit.log(ctx, {
userId: user._id, // From parent's users table
action: "delete",
resourceId: task._id, // From parent's tasks table
});
// Component stores these as strings/IDs
// but doesn't access parent tables directly
// Inside component code - DON'T DO THIS
const user = await ctx.db.get(userId); // Error! Can't access parent tables
Components can't call each other directly. If you need this, they should be in the main app or refactor the design.
// convex.config.ts
export default defineApp({
components: {
auth: "@convex-dev/better-auth",
organizations: "./components/organizations",
billing: "./components/billing",
storage: "@convex-dev/r2",
analytics: "./components/analytics",
emails: "./components/emails",
},
});
Each component:
auth - User authentication & sessionsorganizations - Tenant isolation & permissionsbilling - Stripe integration & subscriptionsstorage - File uploads to R2analytics - Event tracking & metricsemails - Email sending via SendGridexport default defineApp({
components: {
cart: "./components/cart",
inventory: "./components/inventory",
orders: "./components/orders",
payments: "@convex-dev/polar",
shipping: "./components/shipping",
recommendations: "./components/recommendations",
},
});
export default defineApp({
components: {
agent: "@convex-dev/agent",
embeddings: "./components/embeddings",
documents: "./components/documents",
chat: "./components/chat",
workflow: "@convex-dev/workflow",
},
});
Step 1: Identify Features
Current monolith:
- File uploads (mixed with main app)
- Rate limiting (scattered everywhere)
- Analytics (embedded in functions)
Step 2: Extract One Feature
# Create component
mkdir -p convex/components/storage
# Move storage code to component
# Update imports in main app
Step 3: Test Independently
# Component has its own tests
# No coupling to main app
Step 4: Repeat Extract other features incrementally.
Each component does ONE thing well:
// Export only what's needed
export { upload, download, delete } from "./storage";
// Keep internals private
// (Don't export helper functions)
// ✅ Good: Pass data as arguments
await components.audit.log(ctx, {
userId: user._id,
action: "delete"
});
// ❌ Bad: Component accesses parent tables
// (Not even possible, but shows the principle)
{
"name": "@yourteam/notifications-component",
"version": "1.0.0"
}
Include README with:
# Make sure component is in convex.config.ts
# Run: npx convex dev
This is by design! Components are sandboxed.
Pass data as arguments instead.
Each component has isolated tables.
Components can't see each other's data.
npm install @convex-dev/component-nameconvex.config.tsRemember: Components are about encapsulation and reusability. When in doubt, prefer components over monolithic code!
Weekly Installs
81
Repository
GitHub Stars
90
First Seen
Feb 6, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
claude-code65
codex65
opencode47
gemini-cli47
github-copilot47
amp46
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
120,000 周安装