
convex-core
Core Convex development guidelines - functions, validators, schema, queries, mutations, and database patterns. Use when working with convex/**/*.ts files or when discussing Convex queries, mutations, schemas, validators, indexes, ctx.db, defineTable, defineSchema, or database patterns.
Core Convex development guidelines - functions, validators, schema, queries, mutations, and database patterns. Use when working with convex/**/*.ts files or when discussing Convex queries, mutations, schemas, validators, indexes, ctx.db, defineTable, defineSchema, or database patterns.
Convex Core Development Guide
Complete guidelines for Convex functions, validators, schema, queries, mutations, and database patterns.
Function Syntax (REQUIRED)
ALWAYS use the new function syntax:
import { query } from "./_generated/server";
import { v } from "convex/values";
export const myFunction = query({
args: {
// Arguments with validators
},
returns: v.null(), // Return validator REQUIRED
handler: async (ctx, args) => {
// Function body
},
});
Validators Reference
| Type | Validator | Notes |
|---|---|---|
| Id | v.id(tableName) |
Document ID |
| Null | v.null() |
Use instead of undefined |
| Int64 | v.int64() |
NOT v.bigint() (deprecated) |
| Float64 | v.number() |
|
| Boolean | v.boolean() |
|
| String | v.string() |
Max 1MB UTF-8 |
| Bytes | v.bytes() |
Max 1MB |
| Array | v.array(values) |
Max 8192 elements |
| Object | v.object({...}) |
Max 1024 entries |
| Record | v.record(keys, values) |
Dynamic keys |
| Optional | v.optional(validator) |
|
| Union | v.union(v1, v2, ...) |
|
| Literal | v.literal("value") |
For discriminated unions |
NOT SUPPORTED: v.map(), v.set(), v.bigint()
Function Registration
Public Functions
import { query, mutation, action } from "./_generated/server";
import { api } from "./_generated/api";
// Reference: api.filename.functionName
Internal Functions (Private)
import { internalQuery, internalMutation, internalAction } from "./_generated/server";
import { internal } from "./_generated/api";
// Reference: internal.filename.functionName
Critical Rules
- ALWAYS include
argsandreturnsvalidators - If no return value, use
returns: v.null() - File-based routing:
convex/users.ts→api.users.functionName
Schema Definition
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
users: defineTable({
name: v.string(),
email: v.string(),
role: v.optional(v.union(v.literal("admin"), v.literal("user"))),
})
.index("by_email", ["email"])
.index("by_role", ["role"]),
messages: defineTable({
userId: v.id("users"),
content: v.string(),
channelId: v.id("channels"),
})
.index("by_channel", ["channelId"])
.index("by_user_and_channel", ["userId", "channelId"]),
});
System Fields (Automatic)
_id:v.id(tableName)_creationTime:v.number()
Index Rules (CRITICAL)
// WRONG - Will cause error!
.index("by_creation_time", ["_creationTime"]) // Built-in, don't add
.index("by_author_and_time", ["author", "_creationTime"]) // _creationTime is automatic
// CORRECT
.index("by_author", ["author"]) // _creationTime added automatically
.index("by_channel_and_author", ["channelId", "authorId"])
- Index names describe all fields:
by_field1_and_field2 - Query order must match index order
- Never include
_creationTimein index definition
Query Patterns
Using Indexes (REQUIRED)
// CORRECT - Use withIndex
const messages = await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", channelId))
.order("desc")
.take(10);
// WRONG - Never use filter()
const messages = await ctx.db
.query("messages")
.filter((q) => q.eq(q.field("channelId"), channelId)) // BAD!
.collect();
Get by ID
const user = await ctx.db.get(userId);
if (!user) throw new Error("User not found");
Ordering
.order("asc") // Ascending (default)
.order("desc") // Descending
Collecting Results
.collect() // Get all results
.take(n) // Get first n results
.first() // Get first result or null
.unique() // Get single result, throws if multiple
Mutations
// Insert - returns Id
const id = await ctx.db.insert("users", { name: "Alice", email: "alice@example.com" });
// Patch - partial update
await ctx.db.patch(userId, { name: "Bob" });
// Replace - full replacement
await ctx.db.replace(userId, { name: "Bob", email: "bob@example.com" });
// Delete
await ctx.db.delete(userId);
Delete Pattern (No .delete() on queries)
const items = await ctx.db
.query("items")
.withIndex("by_user", (q) => q.eq("userId", userId))
.collect();
for (const item of items) {
await ctx.db.delete(item._id);
}
Function Calling
// From mutation or action
const user = await ctx.runQuery(api.users.get, { id: userId });
await ctx.runMutation(internal.users.update, { id: userId, name });
// From action only
await ctx.runAction(internal.ai.generate, { prompt });
Type Annotation for Same-File Calls
export const f = query({
args: { name: v.string() },
returns: v.string(),
handler: async (ctx, args) => "Hello " + args.name,
});
export const g = query({
args: {},
returns: v.null(),
handler: async (ctx) => {
const result: string = await ctx.runQuery(api.example.f, { name: "Bob" });
return null;
},
});
Pagination
import { paginationOptsValidator } from "convex/server";
export const list = query({
args: {
paginationOpts: paginationOptsValidator,
channelId: v.id("channels"),
},
handler: async (ctx, args) => {
return await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.order("desc")
.paginate(args.paginationOpts);
},
});
// Returns: { page, isDone, continueCursor }
Full Text Search
// Schema
defineTable({
body: v.string(),
channel: v.string(),
}).searchIndex("search_body", {
searchField: "body",
filterFields: ["channel"],
})
// Query
const results = await ctx.db
.query("messages")
.withSearchIndex("search_body", (q) =>
q.search("body", "hello").eq("channel", "#general")
)
.take(10);
System Limits
| Limit | Value |
|---|---|
| Function args/returns | 8 MiB |
| Array elements | 8192 |
| Object entries | 1024 |
| Document size | 1 MiB |
| Query/Mutation timeout | 1 second |
| DB read per query | 8 MiB / 16384 docs |
| DB write per mutation | 8 MiB / 8192 docs |
TypeScript Types
import { Id, Doc } from "./_generated/dataModel";
// Use Id<> for document IDs
function getUser(userId: Id<"users">): Promise<Doc<"users"> | null>
// Record with Id keys
const map: Record<Id<"users">, string> = {};
// Arrays with explicit types
const items: Array<{ id: Id<"items">; name: string }> = [];
You Might Also Like
Related Skills

zig-system-calls
Guides using bun.sys for system calls and file I/O in Zig. Use when implementing file operations instead of std.fs or std.posix.
oven-sh
bun-file-io
Use this when you are working on file operations like reading, writing, scanning, or deleting files. It summarizes the preferred file APIs and patterns used in this repo. It also notes when to use filesystem helpers for directories.
anomalyco
vector-index-tuning
Optimize vector index performance for latency, recall, and memory. Use when tuning HNSW parameters, selecting quantization strategies, or scaling vector search infrastructure.
wshobson
similarity-search-patterns
Implement efficient similarity search with vector databases. Use when building semantic search, implementing nearest neighbor queries, or optimizing retrieval performance.
wshobson
dbt-transformation-patterns
Master dbt (data build tool) for analytics engineering with model organization, testing, documentation, and incremental strategies. Use when building data transformations, creating data models, or implementing analytics engineering best practices.
wshobson
event-store-design
Design and implement event stores for event-sourced systems. Use when building event sourcing infrastructure, choosing event store technologies, or implementing event persistence patterns.
wshobson