convex-helpers-guide

Use convex-helpers to add common patterns and utilities to your Convex backend without reinventing the wheel.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "convex-helpers-guide" with this command: npx skills add get-convex/agent-skills/get-convex-agent-skills-convex-helpers-guide

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

  1. 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

  1. 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); }, });

  1. 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.

  1. 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, }); }

  1. 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); }, });

  1. 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

  1. 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

  1. 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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

function-creator

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

components-guide

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

schema-builder

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

convex-quickstart

No summary provided by upstream source.

Repository SourceNeeds Review