Convex Helpers Guide
Use convex-helpers to add common patterns and utilities to your Convex backend without reinventing the wheel.
What is convex-helpers?
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
Available Helpers
- Relationship Helpers
Traverse relationships between tables in a readable, type-safe way.
Use when:
-
Loading related data across tables
-
Following foreign key relationships
-
Building nested data structures
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 document
-
getManyFrom
-
Get multiple related documents
-
getManyVia
-
Get many-to-many relationships through junction table
- Custom Functions (Data Protection) - MOST IMPORTANT
This 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:
-
Data protection and access control (PRIMARY USE CASE)
-
Want to add auth logic to all functions
-
Multi-tenant applications
-
Role-based access control (RBAC)
-
Need to inject common data into ctx
-
Building internal-only functions
-
Adding logging/monitoring to all functions
Why this instead of RLS:
-
TypeScript, not SQL policies
-
Full type safety
-
Easy to test and debug
-
More flexible than database policies
-
Works across your entire backend
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); }, });
- Filter Helper
Apply complex TypeScript filters to database queries.
Use when:
-
Need to filter by computed values
-
Filtering logic is too complex for indexes
-
Working with small result sets
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.
- Sessions
Track users across requests even when not logged in.
Use when:
-
Need to track anonymous users
-
Building shopping cart for guests
-
Tracking user behavior before signup
-
A/B testing without auth
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, }); }
- Zod Validation
Use Zod schemas instead of Convex validators.
Use when:
-
Already using Zod in your project
-
Want more complex validation logic
-
Need custom error messages
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); }, });
- Alternative: Row-Level Security Helper
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.
However, custom functions are usually better because:
-
Type-safe at compile time (RLS is runtime)
-
More explicit (easy to see what auth is applied)
-
Better error messages
-
Easier to test
- Migrations
Run data migrations safely.
Use when:
-
Backfilling new fields
-
Transforming existing data
-
Moving between schema versions
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
- Triggers
Execute code automatically when data changes.
Use when:
-
Sending notifications on data changes
-
Updating related records
-
Logging changes
-
Maintaining computed fields
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, }); });
Common Patterns
Pattern 1: Authenticated Queries with User Context
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(); }, });
Pattern 2: Loading Related Data
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 };
}, });
Pattern 3: Batch Operations with Error Handling
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;
}, });
When to Use What
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
Checklist
-
Installed convex-helpers: npm install convex-helpers
-
Using relationship helpers for related data
-
Created custom functions for common auth patterns
-
Using sessions for anonymous tracking (if needed)
-
Prefer indexes over filter when possible
-
Check convex-helpers docs for new utilities