Skip to main content
The database API provides methods to read and write documents in your Convex database.

Reading data

get

Fetch a single document by its ID.
const user = await ctx.db.get(userId);
if (user === null) {
  throw new Error("User not found");
}
id
Id<TableName>
required
The ID of the document to fetch.
return
Document | null
The document if it exists, or null if it has been deleted or never existed.
You can also specify the table name explicitly:
const user = await ctx.db.get("users", userId);

query

Begin a query for documents in a table.
const messages = await ctx.db
  .query("messages")
  .withIndex("by_channel", (q) => q.eq("channelId", channelId))
  .order("desc")
  .take(50);
tableName
string
required
The name of the table to query.
return
QueryInitializer
A query builder object. See Query methods below.

normalizeId

Validate and normalize an ID string for a specific table.
const userId = ctx.db.normalizeId("users", userIdString);
if (userId === null) {
  throw new Error("Invalid user ID");
}
tableName
string
required
The name of the table.
id
string
required
The ID string to validate.
return
Id<TableName> | null
The normalized ID if valid, or null if the ID is invalid for this table.

Query methods

withIndex

Query documents using an index for efficient lookups.
const tasks = await ctx.db
  .query("tasks")
  .withIndex("by_user", (q) => q.eq("userId", userId))
  .collect();
indexName
string
required
The name of the index to use.
indexRange
function
A function that builds an index range using equality and comparison operators:
  • q.eq(field, value) - Match documents where field equals value
  • q.gt(field, value) - Greater than
  • q.gte(field, value) - Greater than or equal
  • q.lt(field, value) - Less than
  • q.lte(field, value) - Less than or equal
.withIndex("by_status_date", (q) =>
  q.eq("status", "active").gt("createdAt", yesterday)
)

withSearchIndex

Perform full-text search using a search index.
const results = await ctx.db
  .query("documents")
  .withSearchIndex("search_title", (q) =>
    q.search("title", searchTerm)
  )
  .take(20);
indexName
string
required
The name of the search index.
searchFilter
function
required
A function that defines the search query:
  • q.search(field, text) - Full-text search on the search field
  • q.eq(field, value) - Filter by exact match on filter fields
.withSearchIndex("search_content", (q) =>
  q.search("content", query).eq("published", true)
)

order

Define the order of query results.
const recent = await ctx.db
  .query("messages")
  .order("desc")
  .take(10);
order
'asc' | 'desc'
required
The sort order: "asc" for ascending or "desc" for descending.

filter

Filter query results based on a condition.
const completed = await ctx.db
  .query("tasks")
  .filter((q) => q.eq(q.field("completed"), true))
  .collect();
Important: Use .withIndex() instead of .filter() whenever possible. Filters scan all documents matched so far, while indexes efficiently skip non-matching documents.
predicate
function
required
A function that returns a filter expression using:
  • q.eq(a, b) - Equal
  • q.neq(a, b) - Not equal
  • q.lt(a, b) - Less than
  • q.lte(a, b) - Less than or equal
  • q.gt(a, b) - Greater than
  • q.gte(a, b) - Greater than or equal
  • q.and(...) - Logical AND
  • q.or(...) - Logical OR
  • q.not(expr) - Logical NOT
  • q.field(name) - Reference a field
.filter((q) =>
  q.and(
    q.eq(q.field("status"), "active"),
    q.gt(q.field("score"), 100)
  )
)

Consuming query results

collect

Return all query results as an array.
const allUsers = await ctx.db.query("users").collect();
Warning: This loads every matching document into memory. Only use .collect() when the result set is tightly bounded. For large or unbounded result sets, use .take(n), .first(), .unique(), or pagination.

take

Return the first n results.
const recent = await ctx.db
  .query("messages")
  .order("desc")
  .take(50);
n
number
required
The maximum number of results to return.

first

Return the first result, or null if there are no results.
const newest = await ctx.db
  .query("tasks")
  .order("desc")
  .first();

unique

Return the only result, throwing an error if there are multiple matches.
const user = await ctx.db
  .query("users")
  .withIndex("by_email", (q) => q.eq("email", email))
  .unique();
Use this when you expect exactly zero or one result, such as when querying by a unique field. If the query matches more than one document, this will throw an error.

paginate

Load a page of results with a cursor for loading more.
const result = await ctx.db
  .query("messages")
  .order("desc")
  .paginate(paginationOpts);

// result.page contains the documents
// result.continueCursor is used to fetch the next page
paginationOpts
PaginationOptions
required
numItems
number
required
The target number of items per page.
cursor
string | null
The cursor from the previous page’s result, or null for the first page.
page
Array<Document>
The documents in this page.
continueCursor
string
A cursor to fetch the next page.
isDone
boolean
Whether this is the last page.

Async iteration

Process results one at a time without loading all into memory:
for await (const task of ctx.db.query("tasks")) {
  // Process each task
  console.log(task.text);
}

Writing data

Write operations are only available in mutations (MutationCtx).

insert

Insert a new document into a table.
const taskId = await ctx.db.insert("tasks", {
  text: "Buy groceries",
  completed: false,
});
table
string
required
The name of the table.
value
object
required
The document to insert. Do not include system fields (_id, _creationTime).
return
Id<TableName>
The ID of the newly inserted document.

patch

Shallow merge updates into an existing document.
await ctx.db.patch(taskId, { completed: true });

// Remove an optional field
await ctx.db.patch(taskId, { assignee: undefined });
id
Id<TableName>
required
The ID of the document to update.
value
Partial<Document>
required
The fields to update. Fields set to undefined are removed.
Tip: Use patch for partial updates. Use replace to overwrite the entire document.

replace

Completely replace a document with new values.
await ctx.db.replace(userId, {
  name: "Alice Smith",
  email: "[email protected]",
});
id
Id<TableName>
required
The ID of the document to replace.
value
object
required
The new document. System fields (_id, _creationTime) are preserved automatically.

delete

Delete a document from the database.
await ctx.db.delete(taskId);
id
Id<TableName>
required
The ID of the document to delete.

System tables

Access system tables like _storage using ctx.db.system:
// Get file metadata from _storage system table
const metadata = await ctx.db.system.get(storageId);
// metadata: { _id, _creationTime, sha256, size, contentType? }
System tables include:
  • _storage - File metadata
  • _scheduled_functions - Scheduled function state

Best practices

Use indexes for queries

Always use .withIndex() instead of .filter() for field-based queries. Indexes are dramatically faster.

Avoid unbounded queries

Don’t use .collect() on queries that can return unlimited results. Use .take(n), pagination, or async iteration.

Prefer patch over replace

Use .patch() for partial updates to avoid accidentally removing fields.

Check for null

Always check if .get(), .first(), or .unique() returns null before using the result.

Build docs developers (and LLMs) love