Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Discover and use convex-helpers utilities for relationships, filtering, sessions, custom functions, and more. Use when you need pre-built Convex patterns.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 245% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 128% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 79% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 194% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 137% | 0% |
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:
bashnpm install convex-helpers
Traverse relationships between tables in a readable, type-safe way.
Use when:
Example:
typescriptimport { 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
typescript// 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
typescriptimport { 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
typescriptimport { 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:
typescriptimport { 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:
typescript// 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):
typescriptimport { 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:
typescriptimport { 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):
typescriptimport { 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
typescriptexport 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:
typescriptimport { 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:
typescriptimport { 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:
typescriptimport { 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, }; }, });
typescriptimport { 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(); }, });
typescriptimport { 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 }; }, });
typescriptimport { 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; }, });
authedQuery, authedMutation, etc.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 | | 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 |
Other measured skills in the registry, with their headline benchmark lift.