Convex Patterns

Design your functions

Split Convex queries, mutations, and actions in a way that matches the public frontend API used by this repo.

The frontend helpers in this repo call normal public Convex functions. Keep the function boundary clear so your app code stays predictable.

Use queries for reads

convex/tasks.ts
import { v } from 'convex/values'
import { query } from './_generated/server'

export const list = query({
  args: { userId: v.string() },
  handler: async (ctx, { userId }) => {
    return await ctx.db
      .query('tasks')
      .withIndex('by_user', q => q.eq('userId', userId))
      .order('desc')
      .collect()
  },
})

Queries back useConvexQuery and useConvexQueries.

Use mutations for writes

convex/tasks.ts
import { v } from 'convex/values'
import { mutation } from './_generated/server'

export const add = mutation({
  args: { title: v.string(), userId: v.string() },
  handler: async (ctx, { title, userId }) => {
    return await ctx.db.insert('tasks', {
      title,
      userId,
      isCompleted: false,
      createdAt: Date.now(),
    })
  },
})

Mutations back useConvexMutation and support optimistic updates on the client.

Use actions for orchestration or external work

convex/tasks.ts
import { v } from 'convex/values'
import { api } from './_generated/api'
import { action } from './_generated/server'

export const processAndSave = action({
  args: { text: v.string(), userId: v.string() },
  handler: async (ctx, { text, userId }) => {
    const processed = text.trim()

    await ctx.runMutation(api.tasks.add, {
      title: processed,
      userId,
    })

    return processed
  },
})

Actions back useConvexAction. When an action needs data access, call ctx.runQuery() or ctx.runMutation() explicitly.

Next steps

  • Read Realtime and SSR to understand how query results move from server render to client subscription
  • Read the Vue Guide or Nuxt track to consume these functions from the frontend
Copyright © 2026