Skip to main content
Queries allow you to read data from your Convex database. They are reactive and automatically update when data changes.

Defining queries

query

Define a public query function that can be called from clients.
import { query } from "./_generated/server";
import { v } from "convex/values";

export const listMessages = query({
  args: { channelId: v.id("channels") },
  returns: v.array(v.object({
    _id: v.id("messages"),
    text: v.string(),
    author: v.string(),
  })),
  handler: async (ctx, args) => {
    return await ctx.db
      .query("messages")
      .withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
      .order("desc")
      .take(100);
  },
});
args
PropertyValidators
Argument validation object using validators from convex/values. Maps argument names to their validators.
args: {
  userId: v.id("users"),
  limit: v.optional(v.number()),
}
returns
Validator
Return value validator. Helps catch bugs and provides better type safety.
returns: v.array(v.string())
handler
function
required
The implementation function that receives a QueryCtx and validated arguments.
ctx
QueryCtx
Query context with read-only database access.
args
object
Validated arguments matching the args validator.

internalQuery

Define an internal query that can only be called from other Convex functions, not directly from clients.
import { internalQuery } from "./_generated/server";
import { v } from "convex/values";

export const getUserByEmail = internalQuery({
  args: { email: v.string() },
  returns: v.union(v.id("users"), v.null()),
  handler: async (ctx, args) => {
    const user = await ctx.db
      .query("users")
      .withIndex("by_email", (q) => q.eq("email", args.email))
      .unique();
    return user?._id ?? null;
  },
});
Internal queries are useful for:
  • Queries called from actions or scheduled functions
  • Shared query logic that shouldn’t be exposed to clients
  • Administrative queries

Query context

QueryCtx

The context object passed to query handlers.
db
DatabaseReader
Read-only database interface. See Database API for details.
const messages = await ctx.db.query("messages").collect();
const user = await ctx.db.get(userId);
auth
Auth
Authentication interface to get the current user’s identity.
const identity = await ctx.auth.getUserIdentity();
if (identity === null) {
  throw new Error("Not authenticated");
}
storage
StorageReader
Read-only file storage interface. See Storage API for details.
const url = await ctx.storage.getUrl(storageId);
runQuery
function
Call another query function within the same read snapshot.
const user = await ctx.runQuery(internal.users.getById, { id: userId });
Note: Often you can extract shared logic into a helper function instead. runQuery incurs overhead of running argument and return value validation.

Function shorthand

You can also define queries using function shorthand syntax:
export const myQuery = query(async (ctx, args) => {
  // Query implementation
});
This syntax is more concise but doesn’t provide argument or return value validation.

Best practices

Use indexes for efficient queries

Always use .withIndex() instead of .filter() when querying by specific fields. Filters scan all documents, while indexes efficiently skip non-matching documents.

Add argument validation

For security, always add argument validation to public queries in production apps.

Limit result sets

Use .take(n), .first(), .unique(), or pagination instead of .collect() when result sets can grow unbounded.

Queries are reactive

When used with useQuery on the client, queries automatically re-run when their results change.

Build docs developers (and LLMs) love