重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
convex-helpers-guide by get-convex/convex-agent-plugins
npx skills add https://github.com/get-convex/convex-agent-plugins --skill convex-helpers-guide使用 convex-helpers 为你的 Convex 后端添加常见模式和实用工具,无需重复造轮子。
convex-helpers 是 Convex 的官方工具集。它为常见的后端需求提供了经过实战检验的模式。
安装:
npm install convex-helpers
以可读、类型安全的方式遍历表之间的关系。
使用场景:
示例:
import { getOneFrom, getManyFrom } from "convex-helpers/server/relationships";
export const getTaskWithUser = query({
args: { taskId: v.id("tasks") },
handler: async (ctx, args) => {
const task = await ctx.db.get(args.taskId);
if (!task) return null;
// 获取关联用户
const user = await getOneFrom(
ctx.db,
"users",
"by_id",
task.userId,
"_id"
);
// 获取关联评论
const comments = await getManyFrom(
ctx.db,
"comments",
"by_task",
task._id,
"taskId"
);
return { ...task, user, comments };
},
});
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
关键函数:
getOneFrom - 获取单个关联文档getManyFrom - 获取多个关联文档getManyVia - 通过连接表获取多对多关系这是 Convex 的行级安全(RLS)替代方案。 无需数据库级策略,使用自定义函数包装器自动为所有查询和变更添加身份验证和访问控制。
创建具有自定义行为的查询/变更/操作的包装版本。
使用场景:
为何选择此方案而非 RLS:
示例:带自动身份验证的自定义查询
// convex/lib/customFunctions.ts
import { customQuery } from "convex-helpers/server/customFunctions";
import { query } from "../_generated/server";
export const authenticatedQuery = customQuery(
query,
{
args: {}, // 无需额外参数
input: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Not authenticated");
}
const user = await ctx.db
.query("users")
.withIndex("by_token", q =>
q.eq("tokenIdentifier", identity.tokenIdentifier)
)
.unique();
if (!user) throw new Error("User not found");
// 将用户添加到上下文
return { ctx: { ...ctx, user }, args };
},
}
);
// 在你的函数中使用
export const getMyTasks = authenticatedQuery({
handler: async (ctx) => {
// ctx.user 自动可用!
return await ctx.db
.query("tasks")
.withIndex("by_user", q => q.eq("userId", ctx.user._id))
.collect();
},
});
示例:多租户数据保护
import { customQuery } from "convex-helpers/server/customFunctions";
import { query } from "../_generated/server";
// 组织范围的查询 - 自动访问控制
export const orgQuery = customQuery(query, {
args: { orgId: v.id("organizations") },
input: async (ctx, args) => {
const user = await getCurrentUser(ctx);
// 验证用户是该组织的成员
const member = await ctx.db
.query("organizationMembers")
.withIndex("by_org_and_user", q =>
q.eq("orgId", args.orgId).eq("userId", user._id)
)
.unique();
if (!member) {
throw new Error("Not authorized for this organization");
}
// 注入组织上下文
return {
ctx: {
...ctx,
user,
orgId: args.orgId,
role: member.role
},
args
};
},
});
// 使用 - 数据自动限定在组织范围内
export const getOrgProjects = orgQuery({
args: { orgId: v.id("organizations") },
handler: async (ctx) => {
// ctx.user 和 ctx.orgId 自动可用且已验证!
return await ctx.db
.query("projects")
.withIndex("by_org", q => q.eq("orgId", ctx.orgId))
.collect();
},
});
示例:基于角色的访问控制
import { customMutation } from "convex-helpers/server/customFunctions";
import { mutation } from "../_generated/server";
export const adminMutation = customMutation(mutation, {
args: {},
input: async (ctx, args) => {
const user = await getCurrentUser(ctx);
if (user.role !== "admin") {
throw new Error("Admin access required");
}
return { ctx: { ...ctx, user }, args };
},
});
// 使用 - 只有管理员可以调用此函数
export const deleteUser = adminMutation({
args: { userId: v.id("users") },
handler: async (ctx, args) => {
// 只有管理员能执行到此代码
await ctx.db.delete(args.userId);
},
});
对数据库查询应用复杂的 TypeScript 过滤器。
使用场景:
示例:
import { filter } from "convex-helpers/server/filter";
export const getActiveTasks = query({
handler: async (ctx) => {
const now = Date.now();
const threeDaysAgo = now - 3 * 24 * 60 * 60 * 1000;
return await filter(
ctx.db.query("tasks"),
(task) =>
!task.completed &&
task.createdAt > threeDaysAgo &&
task.priority === "high"
).collect();
},
});
注意: 在可能的情况下仍应优先使用索引!过滤器适用于无法建立索引的复杂逻辑。
即使在用户未登录的情况下,也能跨请求跟踪用户。
使用场景:
设置:
// convex/sessions.ts
import { SessionIdArg } from "convex-helpers/server/sessions";
import { query } from "./_generated/server";
export const trackView = query({
args: {
...SessionIdArg, // 添加 sessionId: v.string()
pageUrl: v.string(),
},
handler: async (ctx, args) => {
await ctx.db.insert("pageViews", {
sessionId: args.sessionId,
pageUrl: args.pageUrl,
timestamp: Date.now(),
});
},
});
客户端(React):
import { useSessionId } from "convex-helpers/react/sessions";
import { useQuery } from "convex/react";
import { api } from "../convex/_generated/api";
function MyComponent() {
const sessionId = useSessionId();
// 自动在所有请求中包含 sessionId
useQuery(api.sessions.trackView, {
sessionId,
pageUrl: window.location.href,
});
}
使用 Zod 模式替代 Convex 验证器。
使用场景:
示例:
import { zCustomQuery } from "convex-helpers/server/zod";
import { z } from "zod";
import { query } from "./_generated/server";
const argsSchema = z.object({
email: z.string().email(),
age: z.number().min(18).max(120),
});
export const createUser = zCustomQuery(query, {
args: argsSchema,
handler: async (ctx, args) => {
// args 根据 Zod 模式进行类型推断
return await ctx.db.insert("users", args);
},
});
注意: Convex 推荐使用自定义函数(见上文第 2 点)作为主要的数据保护模式。这个 RLS 助手是模仿传统 RLS 的替代方法。
使用 RLS 风格的规则实现细粒度的访问控制。
使用场景:
然而,自定义函数通常更好,因为:
示例(如果你偏好 RLS 风格):
import { RowLevelSecurity } from "convex-helpers/server/rowLevelSecurity";
const rules = new RowLevelSecurity();
rules.addRule("tasks", async (ctx, task) => {
const user = await getCurrentUser(ctx);
// 用户只能看到自己的任务
return task.userId === user._id;
});
export const getTasks = query({
handler: async (ctx) => {
return await rules.applyRules(
ctx,
ctx.db.query("tasks").collect()
);
},
});
推荐替代方案:自定义函数
export const myQuery = authedQuery({
handler: async (ctx) => {
// 更明确、类型安全、错误信息更好
return await ctx.db
.query("tasks")
.withIndex("by_user", q => q.eq("userId", ctx.user._id))
.collect();
},
});
安全地运行数据迁移。
使用场景:
示例:
import { makeMigration } from "convex-helpers/server/migrations";
export const addDefaultPriority = makeMigration({
table: "tasks",
migrateOne: async (ctx, doc) => {
if (doc.priority === undefined) {
await ctx.db.patch(doc._id, { priority: "medium" });
}
},
});
// 运行:npx convex run migrations:addDefaultPriority
在数据更改时自动执行代码。
使用场景:
示例:
import { Triggers } from "convex-helpers/server/triggers";
const triggers = new Triggers();
triggers.register("tasks", "insert", async (ctx, task) => {
// 任务创建时发送通知
await ctx.db.insert("notifications", {
userId: task.userId,
type: "task_created",
taskId: task._id,
});
});
高效计算聚合。
示例:
import { aggregation } from "convex-helpers/server/aggregation";
export const getTaskStats = query({
handler: async (ctx) => {
const stats = await aggregation(
ctx.db.query("tasks"),
{
total: "count",
completed: (task) => task.completed ? 1 : 0,
totalPriority: (task) =>
task.priority === "high" ? 3 : task.priority === "medium" ? 2 : 1,
}
);
return {
total: stats.total,
completed: stats.completed,
avgPriority: stats.totalPriority / stats.total,
};
},
});
import { customQuery } from "convex-helpers/server/customFunctions";
export const authedQuery = customQuery(query, {
args: {},
input: async (ctx, args) => {
const user = await getCurrentUser(ctx);
return { ctx: { ...ctx, user }, args };
},
});
// 现在所有查询都自动在上下文中包含用户
export const getMyData = authedQuery({
handler: async (ctx) => {
// ctx.user 已类型化且可用!
return await ctx.db
.query("data")
.withIndex("by_user", q => q.eq("userId", ctx.user._id))
.collect();
},
});
import { getOneFrom, getManyFrom } from "convex-helpers/server/relationships";
export const getPostWithDetails = query({
args: { postId: v.id("posts") },
handler: async (ctx, args) => {
const post = await ctx.db.get(args.postId);
if (!post) return null;
// 加载作者
const author = await getOneFrom(
ctx.db,
"users",
"by_id",
post.authorId,
"_id"
);
// 加载评论
const comments = await getManyFrom(
ctx.db,
"comments",
"by_post",
post._id,
"postId"
);
// 加载标签(多对多)
const tagLinks = await getManyFrom(
ctx.db,
"postTags",
"by_post",
post._id,
"postId"
);
const tags = await Promise.all(
tagLinks.map(link =>
getOneFrom(ctx.db, "tags", "by_id", link.tagId, "_id")
)
);
return { ...post, author, comments, tags };
},
});
import { asyncMap } from "convex-helpers";
export const batchUpdateTasks = mutation({
args: {
taskIds: v.array(v.id("tasks")),
status: v.string(),
},
handler: async (ctx, args) => {
const results = await asyncMap(args.taskIds, async (taskId) => {
try {
const task = await ctx.db.get(taskId);
if (task) {
await ctx.db.patch(taskId, { status: args.status });
return { success: true, taskId };
}
return { success: false, taskId, error: "Not found" };
} catch (error) {
return { success: false, taskId, error: error.message };
}
});
return results;
},
});
从 convex-helpers 开始
使用自定义函数处理身份验证
authedQuery、authedMutation 等使用关系而非嵌套
谨慎使用过滤器
为匿名用户使用会话
npm install convex-helpers| 需求 | 使用 | 导入来源 |
|---|---|---|
| 加载关联数据 | getOneFrom、getManyFrom | convex-helpers/server/relationships |
| 在所有函数中进行身份验证 | customQuery | convex-helpers/server/customFunctions |
| 复杂过滤器 | filter | convex-helpers/server/filter |
| 匿名用户 | useSessionId | convex-helpers/react/sessions |
| Zod 验证 | zCustomQuery | convex-helpers/server/zod |
| 数据迁移 | makeMigration | convex-helpers/server/migrations |
| 触发器 | Triggers | convex-helpers/server/triggers |
每周安装量
52
代码仓库
GitHub 星标数
90
首次出现
2026年2月6日
安全审计
安装于
opencode47
codex47
gemini-cli46
github-copilot46
amp45
kimi-cli45
Use convex-helpers to add common patterns and utilities to your Convex backend without reinventing the wheel.
convex-helpers is the official collection of utilities that complement Convex. It provides battle-tested patterns for common backend needs.
Installation:
npm install convex-helpers
Traverse relationships between tables in a readable, type-safe way.
Use when:
Example:
import { getOneFrom, getManyFrom } from "convex-helpers/server/relationships";
export const getTaskWithUser = query({
args: { taskId: v.id("tasks") },
handler: async (ctx, args) => {
const task = await ctx.db.get(args.taskId);
if (!task) return null;
// Get related user
const user = await getOneFrom(
ctx.db,
"users",
"by_id",
task.userId,
"_id"
);
// Get related comments
const comments = await getManyFrom(
ctx.db,
"comments",
"by_task",
task._id,
"taskId"
);
return { ...task, user, comments };
},
});
Key Functions:
getOneFrom - Get single related documentgetManyFrom - Get multiple related documentsgetManyVia - Get many-to-many relationships through junction tableThis is Convex's alternative to Row Level Security (RLS). Instead of database-level policies, use custom function wrappers to automatically add auth and access control to all queries and mutations.
Create wrapped versions of query/mutation/action with custom behavior.
Use when:
Why this instead of RLS:
Example: Custom Query with Auto-Auth
// convex/lib/customFunctions.ts
import { customQuery } from "convex-helpers/server/customFunctions";
import { query } from "../_generated/server";
export const authenticatedQuery = customQuery(
query,
{
args: {}, // No additional args required
input: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Not authenticated");
}
const user = await ctx.db
.query("users")
.withIndex("by_token", q =>
q.eq("tokenIdentifier", identity.tokenIdentifier)
)
.unique();
if (!user) throw new Error("User not found");
// Add user to context
return { ctx: { ...ctx, user }, args };
},
}
);
// Usage in your functions
export const getMyTasks = authenticatedQuery({
handler: async (ctx) => {
// ctx.user is automatically available!
return await ctx.db
.query("tasks")
.withIndex("by_user", q => q.eq("userId", ctx.user._id))
.collect();
},
});
Example: Multi-Tenant Data Protection
import { customQuery } from "convex-helpers/server/customFunctions";
import { query } from "../_generated/server";
// Organization-scoped query - automatic access control
export const orgQuery = customQuery(query, {
args: { orgId: v.id("organizations") },
input: async (ctx, args) => {
const user = await getCurrentUser(ctx);
// Verify user is a member of this organization
const member = await ctx.db
.query("organizationMembers")
.withIndex("by_org_and_user", q =>
q.eq("orgId", args.orgId).eq("userId", user._id)
)
.unique();
if (!member) {
throw new Error("Not authorized for this organization");
}
// Inject org context
return {
ctx: {
...ctx,
user,
orgId: args.orgId,
role: member.role
},
args
};
},
});
// Usage - data automatically scoped to organization
export const getOrgProjects = orgQuery({
args: { orgId: v.id("organizations") },
handler: async (ctx) => {
// ctx.user and ctx.orgId automatically available and verified!
return await ctx.db
.query("projects")
.withIndex("by_org", q => q.eq("orgId", ctx.orgId))
.collect();
},
});
Example: Role-Based Access Control
import { customMutation } from "convex-helpers/server/customFunctions";
import { mutation } from "../_generated/server";
export const adminMutation = customMutation(mutation, {
args: {},
input: async (ctx, args) => {
const user = await getCurrentUser(ctx);
if (user.role !== "admin") {
throw new Error("Admin access required");
}
return { ctx: { ...ctx, user }, args };
},
});
// Usage - only admins can call this
export const deleteUser = adminMutation({
args: { userId: v.id("users") },
handler: async (ctx, args) => {
// Only admins reach this code
await ctx.db.delete(args.userId);
},
});
Apply complex TypeScript filters to database queries.
Use when:
Example:
import { filter } from "convex-helpers/server/filter";
export const getActiveTasks = query({
handler: async (ctx) => {
const now = Date.now();
const threeDaysAgo = now - 3 * 24 * 60 * 60 * 1000;
return await filter(
ctx.db.query("tasks"),
(task) =>
!task.completed &&
task.createdAt > threeDaysAgo &&
task.priority === "high"
).collect();
},
});
Note: Still prefer indexes when possible! Use filter for complex logic that can't be indexed.
Track users across requests even when not logged in.
Use when:
Setup:
// convex/sessions.ts
import { SessionIdArg } from "convex-helpers/server/sessions";
import { query } from "./_generated/server";
export const trackView = query({
args: {
...SessionIdArg, // Adds sessionId: v.string()
pageUrl: v.string(),
},
handler: async (ctx, args) => {
await ctx.db.insert("pageViews", {
sessionId: args.sessionId,
pageUrl: args.pageUrl,
timestamp: Date.now(),
});
},
});
Client (React):
import { useSessionId } from "convex-helpers/react/sessions";
import { useQuery } from "convex/react";
import { api } from "../convex/_generated/api";
function MyComponent() {
const sessionId = useSessionId();
// Automatically includes sessionId in all requests
useQuery(api.sessions.trackView, {
sessionId,
pageUrl: window.location.href,
});
}
Use Zod schemas instead of Convex validators.
Use when:
Example:
import { zCustomQuery } from "convex-helpers/server/zod";
import { z } from "zod";
import { query } from "./_generated/server";
const argsSchema = z.object({
email: z.string().email(),
age: z.number().min(18).max(120),
});
export const createUser = zCustomQuery(query, {
args: argsSchema,
handler: async (ctx, args) => {
// args is typed from Zod schema
return await ctx.db.insert("users", args);
},
});
Note: Convex recommends using custom functions (see #2 above) as the primary data protection pattern. This RLS helper is an alternative approach that mimics traditional RLS.
Implement fine-grained access control with RLS-style rules.
Use when:
However, custom functions are usually better because:
Example (if you prefer RLS style):
import { RowLevelSecurity } from "convex-helpers/server/rowLevelSecurity";
const rules = new RowLevelSecurity();
rules.addRule("tasks", async (ctx, task) => {
const user = await getCurrentUser(ctx);
// Users can only see their own tasks
return task.userId === user._id;
});
export const getTasks = query({
handler: async (ctx) => {
return await rules.applyRules(
ctx,
ctx.db.query("tasks").collect()
);
},
});
Recommended instead: Custom functions
export const myQuery = authedQuery({
handler: async (ctx) => {
// More explicit, type-safe, better errors
return await ctx.db
.query("tasks")
.withIndex("by_user", q => q.eq("userId", ctx.user._id))
.collect();
},
});
Run data migrations safely.
Use when:
Example:
import { makeMigration } from "convex-helpers/server/migrations";
export const addDefaultPriority = makeMigration({
table: "tasks",
migrateOne: async (ctx, doc) => {
if (doc.priority === undefined) {
await ctx.db.patch(doc._id, { priority: "medium" });
}
},
});
// Run: npx convex run migrations:addDefaultPriority
Execute code automatically when data changes.
Use when:
Example:
import { Triggers } from "convex-helpers/server/triggers";
const triggers = new Triggers();
triggers.register("tasks", "insert", async (ctx, task) => {
// Send notification when task is created
await ctx.db.insert("notifications", {
userId: task.userId,
type: "task_created",
taskId: task._id,
});
});
Compute aggregates efficiently.
Example:
import { aggregation } from "convex-helpers/server/aggregation";
export const getTaskStats = query({
handler: async (ctx) => {
const stats = await aggregation(
ctx.db.query("tasks"),
{
total: "count",
completed: (task) => task.completed ? 1 : 0,
totalPriority: (task) =>
task.priority === "high" ? 3 : task.priority === "medium" ? 2 : 1,
}
);
return {
total: stats.total,
completed: stats.completed,
avgPriority: stats.totalPriority / stats.total,
};
},
});
import { customQuery } from "convex-helpers/server/customFunctions";
export const authedQuery = customQuery(query, {
args: {},
input: async (ctx, args) => {
const user = await getCurrentUser(ctx);
return { ctx: { ...ctx, user }, args };
},
});
// Now all queries automatically have user in context
export const getMyData = authedQuery({
handler: async (ctx) => {
// ctx.user is typed and available!
return await ctx.db
.query("data")
.withIndex("by_user", q => q.eq("userId", ctx.user._id))
.collect();
},
});
import { getOneFrom, getManyFrom } from "convex-helpers/server/relationships";
export const getPostWithDetails = query({
args: { postId: v.id("posts") },
handler: async (ctx, args) => {
const post = await ctx.db.get(args.postId);
if (!post) return null;
// Load author
const author = await getOneFrom(
ctx.db,
"users",
"by_id",
post.authorId,
"_id"
);
// Load comments
const comments = await getManyFrom(
ctx.db,
"comments",
"by_post",
post._id,
"postId"
);
// Load tags (many-to-many)
const tagLinks = await getManyFrom(
ctx.db,
"postTags",
"by_post",
post._id,
"postId"
);
const tags = await Promise.all(
tagLinks.map(link =>
getOneFrom(ctx.db, "tags", "by_id", link.tagId, "_id")
)
);
return { ...post, author, comments, tags };
},
});
import { asyncMap } from "convex-helpers";
export const batchUpdateTasks = mutation({
args: {
taskIds: v.array(v.id("tasks")),
status: v.string(),
},
handler: async (ctx, args) => {
const results = await asyncMap(args.taskIds, async (taskId) => {
try {
const task = await ctx.db.get(taskId);
if (task) {
await ctx.db.patch(taskId, { status: args.status });
return { success: true, taskId };
}
return { success: false, taskId, error: "Not found" };
} catch (error) {
return { success: false, taskId, error: error.message };
}
});
return results;
},
});
Start with convex-helpers
Custom Functions for Auth
authedQuery, authedMutation, etc.Relationships Over Nesting
Filter Sparingly
Sessions for Anonymous Users
npm install convex-helpers| Need | Use | Import From |
|---|---|---|
| Load related data | getOneFrom, getManyFrom | convex-helpers/server/relationships |
| Auth in all functions | customQuery | convex-helpers/server/customFunctions |
| Complex filters | filter | convex-helpers/server/filter |
Weekly Installs
52
Repository
GitHub Stars
90
First Seen
Feb 6, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode47
codex47
gemini-cli46
github-copilot46
amp45
kimi-cli45
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
125,600 周安装
| Anonymous users | useSessionId | convex-helpers/react/sessions |
| Zod validation | zCustomQuery | convex-helpers/server/zod |
| Data migrations | makeMigration | convex-helpers/server/migrations |
| Triggers | Triggers | convex-helpers/server/triggers |