with one click
convex-helpers-guide
// Discover and use convex-helpers utilities for relationships, filtering, sessions, custom functions, and more. Use when you need pre-built Convex patterns.
// Discover and use convex-helpers utilities for relationships, filtering, sessions, custom functions, and more. Use when you need pre-built Convex patterns.
Set up Convex authentication with proper user management, identity mapping, and access control patterns. Use when implementing auth flows.
Guide to using Convex components for feature encapsulation. Learn about sibling components, creating your own, and when to use components vs monolithic code.
Initialize a new Convex backend from scratch with schema, auth, and basic CRUD operations. Use when starting a new project or adding Convex to an existing app.
Create Convex queries, mutations, and actions with proper validation, authentication, and error handling. Use when implementing new API endpoints.
Plan and execute Convex schema migrations safely, including adding fields, creating tables, and data transformations. Use when schema changes affect existing data.
Design and generate Convex database schemas with proper validation, indexes, and relationships. Use when creating schema.ts or modifying table definitions.
| name | convex-helpers-guide |
| description | Discover and use convex-helpers utilities for relationships, filtering, sessions, custom functions, and more. Use when you need pre-built Convex patterns. |
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 |
| 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 |