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.
.claude/skills/kunanonj-cursor-plugin-convex-convex-helpers-guide/SKILL.md| 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 |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-02 | fail→pass | 6,726 | 6,867 | +2% | 1 | 1 | 0% | 1,709 | 5,896 | +245% | 0 | 0 | — |
case-03 | fail→pass | 10,504 | 4,873 | -54% | 1 | 1 | 0% | 2,314 | 5,268 | +128% | 0 | 0 | — |
case-01 | fail→pass | 14,830 | 10,475 | -29% | 1 | 1 | 0% | 3,894 | 6,983 | +79% | 0 | 0 | — |
case-04 | fail→pass | 8,964 | 7,186 | -20% | 1 | 1 | 0% | 2,037 | 5,992 | +194% | 0 | 0 | — |
case-05 | pass→pass | 10,133 | 5,251 | -48% | 1 | 1 | 0% | 2,467 | 5,441 | +121% | 0 | 0 | — |
case-06 | fail→pass | 9,966 | 5,343 | -46% | 1 | 1 | 0% | 2,301 | 5,464 | +137% | 0 | 0 | — |
case-07 | fail→pass | 6,787 | 3,294 | -51% | 1 | 1 | 0% | 1,539 | 4,971 | +223% | 0 | 0 | — |
case-08 | fail→pass | 12,352 | 10,600 | -14% | 1 | 1 | 0% | 3,046 | 6,538 | +115% | 0 | 0 | — |
case-09 | pass→pass | 15,852 | 5,854 | -63% | 1 | 1 | 0% | 3,238 | 5,616 | +73% | 0 | 0 | — |
case-10 | pass→pass | 13,779 | 9,173 | -33% | 1 | 1 | 0% | 2,581 | 6,127 | +137% | 0 | 0 | — |
case-11 | pass→pass | 7,931 | 3,163 | -60% | 1 | 1 | 0% | 1,734 | 4,944 | +185% | 0 | 0 | — |
case-12 | fail→pass | 10,222 | 4,460 | -56% | 1 | 1 | 0% | 2,278 | 5,186 | +128% | 0 | 0 | — |
case-13 | fail→pass | 10,719 | 5,065 | -53% | 1 | 1 | 0% | 2,029 | 5,454 | +169% | 0 | 0 | — |
case-14 | pass→pass | 12,471 | 7,375 | -41% | 1 | 1 | 0% | 2,443 | 5,701 | +133% | 0 | 0 | — |
case-15 | pass→pass | 11,524 | 12,538 | +9% | 1 | 1 | 0% | 2,087 | 6,689 | +221% | 0 | 0 | — |
case-16 | fail→pass | 9,130 | 2,038 | -78% | 1 | 1 | 0% | 1,642 | 4,672 | +185% | 0 | 0 | — |
case-17 | pass→pass | 2,319 | 1,134 | -51% | 1 | 1 | 0% | 468 | 4,358 | +831% | 0 | 0 | — |
case-18 | fail→pass | 11,437 | 3,630 | -68% | 1 | 1 | 0% | 2,480 | 5,040 | +103% | 0 | 0 | — |
case-19 | pass→pass | 8,095 | 4,308 | -47% | 1 | 1 | 0% | 1,748 | 5,137 | +194% | 0 | 0 | — |
case-20 | pass→pass | 3,780 | 3,083 | -18% | 1 | 1 | 0% | 911 | 4,861 | +434% | 0 | 0 | — |
case-21 | pass→pass | 3,507 | 3,820 | +9% | 1 | 1 | 0% | 802 | 4,996 | +523% | 0 | 0 | — |
case-22 | pass→pass | 4,271 | 3,080 | -28% | 1 | 1 | 0% | 953 | 4,795 | +403% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +50 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.