Convex Rules
Function guidelines
New function syntax
- ALWAYS use the new function syntax for Convex functions. For example:
import { query } from "./_generated/server";
import { v } from "convex/values";
export const f = query({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
// Function body
},
});
Http endpoint syntax
- HTTP endpoints are defined in
convex/http.ts and require an httpAction decorator. For example:
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
const http = httpRouter();
http.route({
path: "/echo",
method: "POST",
handler: httpAction(async (ctx, req) => {
const body = await req.bytes();
return new Response(body, { status: 200 });
}),
});
- HTTP endpoints are always registered at the exact path you specify in the
path field. For example, if you specify /api/someRoute, the endpoint will be registered at /api/someRoute.
Validators
- Below is an example of an array validator:
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export default mutation({
args: {
simpleArray: v.array(v.union(v.string(), v.number())),
},
handler: async (ctx, args) => {
//...
},
});
- Below is an example of a schema with validators that codify a discriminated union type:
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
results: defineTable(
v.union(
v.object({
kind: v.literal("error"),
errorMessage: v.string(),
}),
v.object({
kind: v.literal("success"),
value: v.number(),
}),
),
)
});
- Always use the
v.null() validator when returning a null value. Below is an example query that returns a null value:
import { query } from "./_generated/server";
import { v } from "convex/values";
export const exampleQuery = query({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
console.log("This query returns a null value");
return null;
},
});
- Here are the valid Convex types along with their respective validators:
| Convex Type |
TS/JS type |
Example Usage |
Validator for argument validation and schemas |
Notes |
| Id |
string |
doc._id |
v.id(tableName) |
|
| Null |
null |
null |
v.null() |
JavaScript's undefined is not a valid Convex value. Functions the return undefined or do not return will return null when called from a client. Use null instead. |
| Int64 |
bigint |
3n |
v.int64() |
Int64s only support BigInts between -2^63 and 2^63-1. Convex supports bigints in most modern browsers. |
| Float64 |
number |
3.1 |
v.number() |
Convex supports all IEEE-754 double-precision floating point numbers (such as NaNs). Inf and NaN are JSON serialized as strings. |
| Boolean |
boolean |
true |
v.boolean() |
|
| String |
string |
"abc" |
v.string() |
Strings are stored as UTF-8 and must be valid Unicode sequences. Strings must be smaller than the 1MB total size limit when encoded as UTF-8. |
| Bytes |
ArrayBuffer |
new ArrayBuffer(8) |
v.bytes() |
Convex supports first class bytestrings, passed in as ArrayBuffers. Bytestrings must be smaller than the 1MB total size limit for Convex types. |
| Array |
Array |
[1, 3.2, "abc"] |
v.array(values) |
Arrays can have at most 8192 values. |
| Object |
Object |
{a: "abc"} |
v.object({property: value}) |
Convex only supports "plain old JavaScript objects" (objects that do not have a custom prototype). Objects can have at most 1024 entries. Field names must be nonempty and not start with "$" or "_". |
| Record |
Record |
{"a": "1", "b": "2"} |
v.record(keys, values) |
Records are objects at runtime, but can have dynamic keys. Keys must be only ASCII characters, nonempty, and not start with "$" or "_". |
Function registration
- Use
internalQuery, internalMutation, and internalAction to register internal functions. These functions are private and aren't part of an app's API. They can only be called by other Convex functions. These functions are always imported from ./_generated/server.
- Use
query, mutation, and action to register public functions. These functions are part of the public API and are exposed to the public Internet. Do NOT use query, mutation, or action to register sensitive internal functions that should be kept private.
- You CANNOT register a function through the
api or internal objects.
- ALWAYS include argument and return validators for all Convex functions. This includes all of
query, internalQuery, mutation, internalMutation, action, and internalAction. If a function doesn't return anything, include returns: v.null() as its output validator.
- If the JavaScript implementation of a Convex function doesn't have a return value, it implicitly returns
null.
Function calling
- Use
ctx.runQuery to call a query from a query, mutation, or action.
- Use
ctx.runMutation to call a mutation from a mutation or action.
- Use
ctx.runAction to call an action from an action.
- ONLY call an action from another action if you need to cross runtimes (e.g. from V8 to Node). Otherwise, pull out the shared code into a helper async function and call that directly instead.
- Try to use as few calls from actions to queries and mutations as possible. Queries and mutations are transactions, so splitting logic up into multiple calls introduces the risk of race conditions.
- All of these calls take in a
FunctionReference. Do NOT try to pass the callee function directly into one of these calls.
- When using
ctx.runQuery, ctx.runMutation, or ctx.runAction to call a function in the same file, specify a type annotation on the return value to work around TypeScript circularity limitations. For example,
export const f = query({
args: { name: v.string() },
returns: v.string(),
handler: async (ctx, args) => {
return "Hello " + args.name;
},
});
export const g = query({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
const result: string = await ctx.runQuery(api.example.f, { name: "Bob" });
return null;
},
});
Function references
- Function references are pointers to registered Convex functions.
- Use the
api object defined by the framework in convex/_generated/api.ts to call public functions registered with query, mutation, or action.
- Use the
internal object defined by the framework in convex/_generated/api.ts to call internal (or private) functions registered with internalQuery, internalMutation, or internalAction.
- Convex uses file-based routing, so a public function defined in
convex/example.ts named f has a function reference of api.example.f.
- A private function defined in
convex/example.ts named g has a function reference of internal.example.g.
- Functions can also registered within directories nested within the
convex/ folder. For example, a public function h defined in convex/messages/access.ts has a function reference of api.messages.access.h.
Api design
- Convex uses file-based routing, so thoughtfully organize files with public query, mutation, or action functions within the
convex/ directory.
- Use
query, mutation, and action to define public functions.
- Use
internalQuery, internalMutation, and internalAction to define private, internal functions.
Pagination
- Paginated queries are queries that return a list of results in incremental pages.
- You can define pagination using the following syntax:
import { v } from "convex/values";
import { query, mutation } from "./_generated/server";
import { paginationOptsValidator } from "convex/server";
export const listWithExtraArg = query({
args: { paginationOpts: paginationOptsValidator, author: v.string() },
handler: async (ctx, args) => {
return await ctx.db
.query("messages")
.filter((q) => q.eq(q.field("author"), args.author))
.order("desc")
.paginate(args.paginationOpts);
},
});
Note: paginationOpts is an object with the following properties:
numItems: the maximum number of documents to return (the validator is v.number())
cursor: the cursor to use to fetch the next page of documents (the validator is v.union(v.string(), v.null()))
- A query that ends in
.paginate() returns an object that has the following properties:
- page (contains an array of documents that you fetches)
- isDone (a boolean that represents whether or not this is the last page of documents)
- continueCursor (a string that represents the cursor to use to fetch the next page of documents)
Validator guidelines
v.bigint() is deprecated for representing signed 64-bit integers. Use v.int64() instead.
- Use
v.record() for defining a record type. v.map() and v.set() are not supported.
Schema guidelines
- Always define your schema in
convex/schema.ts.
- Always import the schema definition functions from
convex/server.
- System fields are automatically added to all documents and are prefixed with an underscore. The two system fields that are automatically added to all documents are
_creationTime which has the validator v.number() and _id which has the validator v.id(tableName).
- Always include all index fields in the index name. For example, if an index is defined as
["field1", "field2"], the index name should be "by_field1_and_field2".
- Index fields must be queried in the same order they are defined. If you want to be able to query by "field1" then "field2" and by "field2" then "field1", you must create separate indexes.
Typescript guidelines
- You can use the helper typescript type
Id imported from './_generated/dataModel' to get the type of the id for a given table. For example if there is a table called 'users' you can use Id<'users'> to get the type of the id for that table.
- If you need to define a
Record make sure that you correctly provide the type of the key and value in the type. For example a validator v.record(v.id('users'), v.string()) would have the type Record<Id<'users'>, string>. Below is an example of using Record with an Id type in a query:
import { query } from "./_generated/server";
import { Doc, Id } from "./_generated/dataModel";
export const exampleQuery = query({
args: { userIds: v.array(v.id("users")) },
returns: v.record(v.id("users"), v.string()),
handler: async (ctx, args) => {
const idToUsername: Record<Id<"users">, string> = {};
for (const userId of args.userIds) {
const user = await ctx.db.get("users", userId);
if (user) {
idToUsername[user._id] = user.username;
}
}
return idToUsername;
},
});
- Be strict with types, particularly around id's of documents. For example, if a function takes in an id for a document in the 'users' table, take in
Id<'users'> rather than string.
- Always use
as const for string literals in discriminated union types.
- When using the
Array type, make sure to always define your arrays as const array: Array<T> = [...];
- When using the
Record type, make sure to always define your records as const record: Record<KeyType, ValueType> = {...};
- Always add
@types/node to your package.json when using any Node.js built-in modules.
Full text search guidelines
- A query for "10 messages in channel '#general' that best match the query 'hello hi' in their body" would look like:
const messages = await ctx.db
.query("messages")
.withSearchIndex("search_body", (q) =>
q.search("body", "hello hi").eq("channel", "#general"),
)
.take(10);
Query guidelines
- Do NOT use
filter in queries. Instead, define an index in the schema and use withIndex instead.
- Convex queries do NOT support
.delete(). Instead, .collect() the results, iterate over them, and call ctx.db.delete(row._id) on each result.
- Use
.unique() to get a single document from a query. This method will throw an error if there are multiple documents that match the query.
- When using async iteration, don't use
.collect() or .take(n) on the result of a query. Instead, use the for await (const row of query) syntax.
Ordering
- By default Convex always returns documents in ascending
_creationTime order.
- You can use
.order('asc') or .order('desc') to pick whether a query is in ascending or descending order. If the order isn't specified, it defaults to ascending.
- Document queries that use indexes will be ordered based on the columns in the index and can avoid slow table scans.
Mutation guidelines
- Use
ctx.db.replace to fully replace an existing document. This method will throw an error if the document does not exist. Syntax: await ctx.db.replace('tasks', taskId, { name: 'Buy milk', completed: false })
- Use
ctx.db.patch to shallow merge updates into an existing document. This method will throw an error if the document does not exist. Syntax: await ctx.db.patch('tasks', taskId, { completed: true })
Action guidelines
- Always add
"use node"; to the top of files containing actions that use Node.js built-in modules.
- Never use
ctx.db inside of an action. Actions don't have access to the database.
- Below is an example of the syntax for an action:
import { action } from "./_generated/server";
export const exampleAction = action({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
console.log("This action does not return anything");
return null;
},
});
Scheduling guidelines
Cron guidelines
- Only use the
crons.interval or crons.cron methods to schedule cron jobs. Do NOT use the crons.hourly, crons.daily, or crons.weekly helpers.
- Both cron methods take in a FunctionReference. Do NOT try to pass the function directly into one of these methods.
- Define crons by declaring the top-level
crons object, calling some methods on it, and then exporting it as default. For example,
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";
import { internalAction } from "./_generated/server";
const empty = internalAction({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
console.log("empty");
},
});
const crons = cronJobs();
// Run `internal.crons.empty` every two hours.
crons.interval("delete inactive users", { hours: 2 }, internal.crons.empty, {});
export default crons;
- You can register Convex functions within
crons.ts just like any other file.
- If a cron calls an internal function, always import the
internal object from '_generated/api', even if the internal function is registered in the same file.
File storage guidelines
Convex includes file storage for large files like images, videos, and PDFs.
The ctx.storage.getUrl() method returns a signed URL for a given file. It returns null if the file doesn't exist.
Do NOT use the deprecated ctx.storage.getMetadata call for loading a file's metadata.
Instead, query the `_storage` system table. For example, you can use `ctx.db.system.get` to get an `Id<"_storage">`.
import { query } from "./_generated/server";
import { Id } from "./_generated/dataModel";
type FileMetadata = {
_id: Id<"_storage">;
_creationTime: number;
contentType?: string;
sha256: string;
size: number;
}
export const exampleQuery = query({
args: { fileId: v.id("_storage") },
returns: v.null(),
handler: async (ctx, args) => {
const metadata: FileMetadata | null = await ctx.db.system.get("_storage", args.fileId);
console.log(metadata);
return null;
},
});
- Convex storage stores items as
Blob objects. You must convert all items to/from a Blob when using Convex storage.
Examples:
Example: chat-app
Task
Create a real-time chat application backend with AI responses. The app should:
- Allow creating users with names
- Support multiple chat channels
- Enable users to send messages to channels
- Automatically generate AI responses to user messages
- Show recent message history
The backend should provide APIs for:
1. User management (creation)
2. Channel management (creation)
3. Message operations (sending, listing)
4. AI response generation using OpenAI's GPT-4
Messages should be stored with their channel, author, and content. The system should maintain message order
and limit history display to the 10 most recent messages per channel.
Analysis
- Task Requirements Summary:
- Build a real-time chat backend with AI integration
- Support user creation
- Enable channel-based conversations
- Store and retrieve messages with proper ordering
- Generate AI responses automatically
- Main Components Needed:
- Database tables: users, channels, messages
- Public APIs for user/channel management
- Message handling functions
- Internal AI response generation system
- Context loading for AI responses
- Public API and Internal Functions Design:
Public Mutations:
- createUser:
- file path: convex/index.ts
- arguments: {name: v.string()}
- returns: v.object({userId: v.id("users")})
- purpose: Create a new user with a given name
- createChannel:
- file path: convex/index.ts
- arguments: {name: v.string()}
- returns: v.object({channelId: v.id("channels")})
- purpose: Create a new channel with a given name
- sendMessage:
- file path: convex/index.ts
- arguments: {channelId: v.id("channels"), authorId: v.id("users"), content: v.string()}
- returns: v.null()
- purpose: Send a message to a channel and schedule a response from the AI
Public Queries:
- listMessages:
- file path: convex/index.ts
- arguments: {channelId: v.id("channels")}
- returns: v.array(v.object({
_id: v.id("messages"),
_creationTime: v.number(),
channelId: v.id("channels"),
authorId: v.optional(v.id("users")),
content: v.string(),
}))
- purpose: List the 10 most recent messages from a channel in descending creation order
Internal Functions:
- generateResponse:
- file path: convex/index.ts
- arguments: {channelId: v.id("channels")}
- returns: v.null()
- purpose: Generate a response from the AI for a given channel
- loadContext:
- file path: convex/index.ts
- arguments: {channelId: v.id("channels")}
- returns: v.array(v.object({
_id: v.id("messages"),
_creationTime: v.number(),
channelId: v.id("channels"),
authorId: v.optional(v.id("users")),
content: v.string(),
}))
- writeAgentResponse:
- file path: convex/index.ts
- arguments: {channelId: v.id("channels"), content: v.string()}
- returns: v.null()
- purpose: Write an AI response to a given channel
- Schema Design:
- users
- validator: { name: v.string() }
- indexes:
- channels
- validator: { name: v.string() }
- indexes:
- messages
- validator: { channelId: v.id("channels"), authorId: v.optional(v.id("users")), content: v.string() }
- indexes
- by_channel: ["channelId"]
- Background Processing:
- AI response generation runs asynchronously after each user message
- Uses OpenAI's GPT-4 to generate contextual responses
- Maintains conversation context using recent message history
Implementation
package.json
{
"name": "chat-app",
"description": "This example shows how to build a chat app without authentication.",
"version": "1.0.0",
"dependencies": {
"convex": "^1.31.2",
"openai": "^4.79.0"
},
"devDependencies": {
"typescript": "^5.7.3"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"allowImportingTsExtensions": true,
"noEmit": true,
"jsx": "react-jsx"
},
"exclude": ["convex"],
"include": ["**/src/**/*.tsx", "**/src/**/*.ts", "vite.config.ts"]
}
convex/index.ts
import {
query,
mutation,
internalQuery,
internalMutation,
internalAction,
} from "./_generated/server";
import { v } from "convex/values";
import OpenAI from "openai";
import { internal } from "./_generated/api";
/**
* Create a user with a given name.
*/
export const createUser = mutation({
args: {
name: v.string(),
},
returns: v.id("users"),
handler: async (ctx, args) => {
return await ctx.db.insert("users", { name: args.name });
},
});
/**
* Create a channel with a given name.
*/
export const createChannel = mutation({
args: {
name: v.string(),
},
returns: v.id("channels"),
handler: async (ctx, args) => {
return await ctx.db.insert("channels", { name: args.name });
},
});
/**
* List the 10 most recent messages from a channel in descending creation order.
*/
export const listMessages = query({
args: {
channelId: v.id("channels"),
},
returns: v.array(
v.object({
_id: v.id("messages"),
_creationTime: v.number(),
channelId: v.id("channels"),
authorId: v.optional(v.id("users")),
content: v.string(),
}),
),
handler: async (ctx, args) => {
const messages = await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.order("desc")
.take(10);
return messages;
},
});
/**
* Send a message to a channel and schedule a response from the AI.
*/
export const sendMessage = mutation({
args: {
channelId: v.id("channels"),
authorId: v.id("users"),
content: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
const channel = await ctx.db.get(args.channelId);
if (!channel) {
throw new Error("Channel not found");
}
const user = await ctx.db.get(args.authorId);
if (!user) {
throw new Error("User not found");
}
await ctx.db.insert("messages", {
channelId: args.channelId,
authorId: args.authorId,
content: args.content,
});
await ctx.scheduler.runAfter(0, internal.index.generateResponse, {
channelId: args.channelId,
});
return null;
},
});
const openai = new OpenAI();
export const generateResponse = internalAction({
args: {
channelId: v.id("channels"),
},
returns: v.null(),
handler: async (ctx, args) => {
const context = await ctx.runQuery(internal.index.loadContext, {
channelId: args.channelId,
});
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: context,
});
const content = response.choices[0].message.content;
if (!content) {
throw new Error("No content in response");
}
await ctx.runMutation(internal.index.writeAgentResponse, {
channelId: args.channelId,
content,
});
return null;
},
});
export const loadContext = internalQuery({
args: {
channelId: v.id("channels"),
},
returns: v.array(
v.object({
role: v.union(v.literal("user"), v.literal("assistant")),
content: v.string(),
}),
),
handler: async (ctx, args) => {
const channel = await ctx.db.get(args.channelId);
if (!channel) {
throw new Error("Channel not found");
}
const messages = await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.order("desc")
.take(10);
const result = [];
for (const message of messages) {
if (message.authorId) {
const user = await ctx.db.get(message.authorId);
if (!user) {
throw new Error("User not found");
}
result.push({
role: "user" as const,
content: `${user.name}: ${message.content}`,
});
} else {
result.push({ role: "assistant" as const, content: message.content });
}
}
return result;
},
});
export const writeAgentResponse = internalMutation({
args: {
channelId: v.id("channels"),
content: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.insert("messages", {
channelId: args.channelId,
content: args.content,
});
return null;
},
});
convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
channels: defineTable({
name: v.string(),
}),
users: defineTable({
name: v.string(),
}),
messages: defineTable({
channelId: v.id("channels"),
authorId: v.optional(v.id("users")),
content: v.string(),
}).index("by_channel", ["channelId"]),
});
convex/tsconfig.json
{
/* This TypeScript project config describes the environment that
* Convex functions run in and is used to typecheck them.
* You can modify it, but some settings required to use Convex.
*/
"compilerOptions": {
/* These settings are not required by Convex and can be modified. */
"allowJs": true,
"strict": true,
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
/* These compiler options are required by Convex */
"target": "ESNext",
"lib": ["ES2021", "dom"],
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"isolatedModules": true,
"noEmit": true
},
"include": ["./**/*"],
"exclude": ["./_generated"]
}
src/App.tsx
export default function App() {
return <div>Hello World</div>;
}
1---2name: convex-rules3description: Understand and use Convex rules whenever touching the backend code, database, or query or mutation functions. Use when writing new functions, updating existing functions, or reading the database.4---56# Convex Rules78## Function guidelines910### New function syntax11- ALWAYS use the new function syntax for Convex functions. For example:12```typescript13import { query } from "./_generated/server";14import { v } from "convex/values";15export const f = query({16 args: {},17 returns: v.null(),18 handler: async (ctx, args) => {19 // Function body20 },21});22```2324### Http endpoint syntax25- HTTP endpoints are defined in `convex/http.ts` and require an `httpAction` decorator. For example:26```typescript27import { httpRouter } from "convex/server";28import { httpAction } from "./_generated/server";29const http = httpRouter();30http.route({31 path: "/echo",32 method: "POST",33 handler: httpAction(async (ctx, req) => {34 const body = await req.bytes();35 return new Response(body, { status: 200 });36 }),37});38```39- HTTP endpoints are always registered at the exact path you specify in the `path` field. For example, if you specify `/api/someRoute`, the endpoint will be registered at `/api/someRoute`.4041### Validators42- Below is an example of an array validator:43```typescript44import { mutation } from "./_generated/server";45import { v } from "convex/values";4647export default mutation({48args: {49 simpleArray: v.array(v.union(v.string(), v.number())),50},51handler: async (ctx, args) => {52 //...53},54});55```56- Below is an example of a schema with validators that codify a discriminated union type:57```typescript58import { defineSchema, defineTable } from "convex/server";59import { v } from "convex/values";6061export default defineSchema({62 results: defineTable(63 v.union(64 v.object({65 kind: v.literal("error"),66 errorMessage: v.string(),67 }),68 v.object({69 kind: v.literal("success"),70 value: v.number(),71 }),72 ),73 )74});75```76- Always use the `v.null()` validator when returning a null value. Below is an example query that returns a null value:77```typescript78import { query } from "./_generated/server";79import { v } from "convex/values";8081export const exampleQuery = query({82 args: {},83 returns: v.null(),84 handler: async (ctx, args) => {85 console.log("This query returns a null value");86 return null;87 },88});89```90- Here are the valid Convex types along with their respective validators:91Convex Type | TS/JS type | Example Usage | Validator for argument validation and schemas | Notes |92| ----------- | ------------| -----------------------| -----------------------------------------------| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|93| Id | string | `doc._id` | `v.id(tableName)` | |94| Null | null | `null` | `v.null()` | JavaScript's `undefined` is not a valid Convex value. Functions the return `undefined` or do not return will return `null` when called from a client. Use `null` instead. |95| Int64 | bigint | `3n` | `v.int64()` | Int64s only support BigInts between -2^63 and 2^63-1. Convex supports `bigint`s in most modern browsers. |96| Float64 | number | `3.1` | `v.number()` | Convex supports all IEEE-754 double-precision floating point numbers (such as NaNs). Inf and NaN are JSON serialized as strings. |97| Boolean | boolean | `true` | `v.boolean()` |98| String | string | `"abc"` | `v.string()` | Strings are stored as UTF-8 and must be valid Unicode sequences. Strings must be smaller than the 1MB total size limit when encoded as UTF-8. |99| Bytes | ArrayBuffer | `new ArrayBuffer(8)` | `v.bytes()` | Convex supports first class bytestrings, passed in as `ArrayBuffer`s. Bytestrings must be smaller than the 1MB total size limit for Convex types. |100| Array | Array | `[1, 3.2, "abc"]` | `v.array(values)` | Arrays can have at most 8192 values. |101| Object | Object | `{a: "abc"}` | `v.object({property: value})` | Convex only supports "plain old JavaScript objects" (objects that do not have a custom prototype). Objects can have at most 1024 entries. Field names must be nonempty and not start with "$" or "_". |102| Record | Record | `{"a": "1", "b": "2"}` | `v.record(keys, values)` | Records are objects at runtime, but can have dynamic keys. Keys must be only ASCII characters, nonempty, and not start with "$" or "_". |103104### Function registration105- Use `internalQuery`, `internalMutation`, and `internalAction` to register internal functions. These functions are private and aren't part of an app's API. They can only be called by other Convex functions. These functions are always imported from `./_generated/server`.106- Use `query`, `mutation`, and `action` to register public functions. These functions are part of the public API and are exposed to the public Internet. Do NOT use `query`, `mutation`, or `action` to register sensitive internal functions that should be kept private.107- You CANNOT register a function through the `api` or `internal` objects.108- ALWAYS include argument and return validators for all Convex functions. This includes all of `query`, `internalQuery`, `mutation`, `internalMutation`, `action`, and `internalAction`. If a function doesn't return anything, include `returns: v.null()` as its output validator.109- If the JavaScript implementation of a Convex function doesn't have a return value, it implicitly returns `null`.110111### Function calling112- Use `ctx.runQuery` to call a query from a query, mutation, or action.113- Use `ctx.runMutation` to call a mutation from a mutation or action.114- Use `ctx.runAction` to call an action from an action.115- ONLY call an action from another action if you need to cross runtimes (e.g. from V8 to Node). Otherwise, pull out the shared code into a helper async function and call that directly instead.116- Try to use as few calls from actions to queries and mutations as possible. Queries and mutations are transactions, so splitting logic up into multiple calls introduces the risk of race conditions.117- All of these calls take in a `FunctionReference`. Do NOT try to pass the callee function directly into one of these calls.118- When using `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction` to call a function in the same file, specify a type annotation on the return value to work around TypeScript circularity limitations. For example,119```120export const f = query({121 args: { name: v.string() },122 returns: v.string(),123 handler: async (ctx, args) => {124 return "Hello " + args.name;125 },126});127128export const g = query({129 args: {},130 returns: v.null(),131 handler: async (ctx, args) => {132 const result: string = await ctx.runQuery(api.example.f, { name: "Bob" });133 return null;134 },135});136```137138### Function references139- Function references are pointers to registered Convex functions.140- Use the `api` object defined by the framework in `convex/_generated/api.ts` to call public functions registered with `query`, `mutation`, or `action`.141- Use the `internal` object defined by the framework in `convex/_generated/api.ts` to call internal (or private) functions registered with `internalQuery`, `internalMutation`, or `internalAction`.142- Convex uses file-based routing, so a public function defined in `convex/example.ts` named `f` has a function reference of `api.example.f`.143- A private function defined in `convex/example.ts` named `g` has a function reference of `internal.example.g`.144- Functions can also registered within directories nested within the `convex/` folder. For example, a public function `h` defined in `convex/messages/access.ts` has a function reference of `api.messages.access.h`.145146### Api design147- Convex uses file-based routing, so thoughtfully organize files with public query, mutation, or action functions within the `convex/` directory.148- Use `query`, `mutation`, and `action` to define public functions.149- Use `internalQuery`, `internalMutation`, and `internalAction` to define private, internal functions.150151### Pagination152- Paginated queries are queries that return a list of results in incremental pages.153- You can define pagination using the following syntax:154155```ts156import { v } from "convex/values";157import { query, mutation } from "./_generated/server";158import { paginationOptsValidator } from "convex/server";159export const listWithExtraArg = query({160 args: { paginationOpts: paginationOptsValidator, author: v.string() },161 handler: async (ctx, args) => {162 return await ctx.db163 .query("messages")164 .filter((q) => q.eq(q.field("author"), args.author))165 .order("desc")166 .paginate(args.paginationOpts);167 },168});169```170Note: `paginationOpts` is an object with the following properties:171- `numItems`: the maximum number of documents to return (the validator is `v.number()`)172- `cursor`: the cursor to use to fetch the next page of documents (the validator is `v.union(v.string(), v.null())`)173- A query that ends in `.paginate()` returns an object that has the following properties:174 - page (contains an array of documents that you fetches)175 - isDone (a boolean that represents whether or not this is the last page of documents)176 - continueCursor (a string that represents the cursor to use to fetch the next page of documents)177178179## Validator guidelines180- `v.bigint()` is deprecated for representing signed 64-bit integers. Use `v.int64()` instead.181- Use `v.record()` for defining a record type. `v.map()` and `v.set()` are not supported.182183## Schema guidelines184- Always define your schema in `convex/schema.ts`.185- Always import the schema definition functions from `convex/server`.186- System fields are automatically added to all documents and are prefixed with an underscore. The two system fields that are automatically added to all documents are `_creationTime` which has the validator `v.number()` and `_id` which has the validator `v.id(tableName)`.187- Always include all index fields in the index name. For example, if an index is defined as `["field1", "field2"]`, the index name should be "by_field1_and_field2".188- Index fields must be queried in the same order they are defined. If you want to be able to query by "field1" then "field2" and by "field2" then "field1", you must create separate indexes.189190## Typescript guidelines191- You can use the helper typescript type `Id` imported from './_generated/dataModel' to get the type of the id for a given table. For example if there is a table called 'users' you can use `Id<'users'>` to get the type of the id for that table.192- If you need to define a `Record` make sure that you correctly provide the type of the key and value in the type. For example a validator `v.record(v.id('users'), v.string())` would have the type `Record<Id<'users'>, string>`. Below is an example of using `Record` with an `Id` type in a query:193```ts194import { query } from "./_generated/server";195import { Doc, Id } from "./_generated/dataModel";196197export const exampleQuery = query({198 args: { userIds: v.array(v.id("users")) },199 returns: v.record(v.id("users"), v.string()),200 handler: async (ctx, args) => {201 const idToUsername: Record<Id<"users">, string> = {};202 for (const userId of args.userIds) {203 const user = await ctx.db.get("users", userId);204 if (user) {205 idToUsername[user._id] = user.username;206 }207 }208209 return idToUsername;210 },211});212```213- Be strict with types, particularly around id's of documents. For example, if a function takes in an id for a document in the 'users' table, take in `Id<'users'>` rather than `string`.214- Always use `as const` for string literals in discriminated union types.215- When using the `Array` type, make sure to always define your arrays as `const array: Array<T> = [...];`216- When using the `Record` type, make sure to always define your records as `const record: Record<KeyType, ValueType> = {...};`217- Always add `@types/node` to your `package.json` when using any Node.js built-in modules.218219## Full text search guidelines220- A query for "10 messages in channel '#general' that best match the query 'hello hi' in their body" would look like:221222const messages = await ctx.db223 .query("messages")224 .withSearchIndex("search_body", (q) =>225 q.search("body", "hello hi").eq("channel", "#general"),226 )227 .take(10);228229## Query guidelines230- Do NOT use `filter` in queries. Instead, define an index in the schema and use `withIndex` instead.231- Convex queries do NOT support `.delete()`. Instead, `.collect()` the results, iterate over them, and call `ctx.db.delete(row._id)` on each result.232- Use `.unique()` to get a single document from a query. This method will throw an error if there are multiple documents that match the query.233- When using async iteration, don't use `.collect()` or `.take(n)` on the result of a query. Instead, use the `for await (const row of query)` syntax.234### Ordering235- By default Convex always returns documents in ascending `_creationTime` order.236- You can use `.order('asc')` or `.order('desc')` to pick whether a query is in ascending or descending order. If the order isn't specified, it defaults to ascending.237- Document queries that use indexes will be ordered based on the columns in the index and can avoid slow table scans.238239240## Mutation guidelines241- Use `ctx.db.replace` to fully replace an existing document. This method will throw an error if the document does not exist. Syntax: `await ctx.db.replace('tasks', taskId, { name: 'Buy milk', completed: false })`242- Use `ctx.db.patch` to shallow merge updates into an existing document. This method will throw an error if the document does not exist. Syntax: `await ctx.db.patch('tasks', taskId, { completed: true })`243244## Action guidelines245- Always add `"use node";` to the top of files containing actions that use Node.js built-in modules.246- Never use `ctx.db` inside of an action. Actions don't have access to the database.247- Below is an example of the syntax for an action:248```ts249import { action } from "./_generated/server";250251export const exampleAction = action({252 args: {},253 returns: v.null(),254 handler: async (ctx, args) => {255 console.log("This action does not return anything");256 return null;257 },258});259```260261## Scheduling guidelines262### Cron guidelines263- Only use the `crons.interval` or `crons.cron` methods to schedule cron jobs. Do NOT use the `crons.hourly`, `crons.daily`, or `crons.weekly` helpers.264- Both cron methods take in a FunctionReference. Do NOT try to pass the function directly into one of these methods.265- Define crons by declaring the top-level `crons` object, calling some methods on it, and then exporting it as default. For example,266```ts267import { cronJobs } from "convex/server";268import { internal } from "./_generated/api";269import { internalAction } from "./_generated/server";270271const empty = internalAction({272 args: {},273 returns: v.null(),274 handler: async (ctx, args) => {275 console.log("empty");276 },277});278279const crons = cronJobs();280281// Run `internal.crons.empty` every two hours.282crons.interval("delete inactive users", { hours: 2 }, internal.crons.empty, {});283284export default crons;285```286- You can register Convex functions within `crons.ts` just like any other file.287- If a cron calls an internal function, always import the `internal` object from '_generated/api', even if the internal function is registered in the same file.288289290## File storage guidelines291- Convex includes file storage for large files like images, videos, and PDFs.292- The `ctx.storage.getUrl()` method returns a signed URL for a given file. It returns `null` if the file doesn't exist.293- Do NOT use the deprecated `ctx.storage.getMetadata` call for loading a file's metadata.294295 Instead, query the `_storage` system table. For example, you can use `ctx.db.system.get` to get an `Id<"_storage">`.296```297import { query } from "./_generated/server";298import { Id } from "./_generated/dataModel";299300type FileMetadata = {301 _id: Id<"_storage">;302 _creationTime: number;303 contentType?: string;304 sha256: string;305 size: number;306}307308export const exampleQuery = query({309 args: { fileId: v.id("_storage") },310 returns: v.null(),311 handler: async (ctx, args) => {312 const metadata: FileMetadata | null = await ctx.db.system.get("_storage", args.fileId);313 console.log(metadata);314 return null;315 },316});317```318- Convex storage stores items as `Blob` objects. You must convert all items to/from a `Blob` when using Convex storage.319320321# Examples:322## Example: chat-app323324### Task325```326Create a real-time chat application backend with AI responses. The app should:327- Allow creating users with names328- Support multiple chat channels329- Enable users to send messages to channels330- Automatically generate AI responses to user messages331- Show recent message history332333The backend should provide APIs for:3341. User management (creation)3352. Channel management (creation)3363. Message operations (sending, listing)3374. AI response generation using OpenAI's GPT-4338339Messages should be stored with their channel, author, and content. The system should maintain message order340and limit history display to the 10 most recent messages per channel.341342```343344### Analysis3451. Task Requirements Summary:346- Build a real-time chat backend with AI integration347- Support user creation348- Enable channel-based conversations349- Store and retrieve messages with proper ordering350- Generate AI responses automatically3513522. Main Components Needed:353- Database tables: users, channels, messages354- Public APIs for user/channel management355- Message handling functions356- Internal AI response generation system357- Context loading for AI responses3583593. Public API and Internal Functions Design:360Public Mutations:361- createUser:362 - file path: convex/index.ts363 - arguments: {name: v.string()}364 - returns: v.object({userId: v.id("users")})365 - purpose: Create a new user with a given name366- createChannel:367 - file path: convex/index.ts368 - arguments: {name: v.string()}369 - returns: v.object({channelId: v.id("channels")})370 - purpose: Create a new channel with a given name371- sendMessage:372 - file path: convex/index.ts373 - arguments: {channelId: v.id("channels"), authorId: v.id("users"), content: v.string()}374 - returns: v.null()375 - purpose: Send a message to a channel and schedule a response from the AI376377Public Queries:378- listMessages:379 - file path: convex/index.ts380 - arguments: {channelId: v.id("channels")}381 - returns: v.array(v.object({382 _id: v.id("messages"),383 _creationTime: v.number(),384 channelId: v.id("channels"),385 authorId: v.optional(v.id("users")),386 content: v.string(),387 }))388 - purpose: List the 10 most recent messages from a channel in descending creation order389390Internal Functions:391- generateResponse:392 - file path: convex/index.ts393 - arguments: {channelId: v.id("channels")}394 - returns: v.null()395 - purpose: Generate a response from the AI for a given channel396- loadContext:397 - file path: convex/index.ts398 - arguments: {channelId: v.id("channels")}399 - returns: v.array(v.object({400 _id: v.id("messages"),401 _creationTime: v.number(),402 channelId: v.id("channels"),403 authorId: v.optional(v.id("users")),404 content: v.string(),405 }))406- writeAgentResponse:407 - file path: convex/index.ts408 - arguments: {channelId: v.id("channels"), content: v.string()}409 - returns: v.null()410 - purpose: Write an AI response to a given channel4114124. Schema Design:413- users414 - validator: { name: v.string() }415 - indexes: <none>416- channels417 - validator: { name: v.string() }418 - indexes: <none>419- messages420 - validator: { channelId: v.id("channels"), authorId: v.optional(v.id("users")), content: v.string() }421 - indexes422 - by_channel: ["channelId"]4234245. Background Processing:425- AI response generation runs asynchronously after each user message426- Uses OpenAI's GPT-4 to generate contextual responses427- Maintains conversation context using recent message history428429430### Implementation431432#### package.json433```typescript434{435 "name": "chat-app",436 "description": "This example shows how to build a chat app without authentication.",437 "version": "1.0.0",438 "dependencies": {439 "convex": "^1.31.2",440 "openai": "^4.79.0"441 },442 "devDependencies": {443 "typescript": "^5.7.3"444 }445}446```447448#### tsconfig.json449```typescript450{451 "compilerOptions": {452 "target": "ESNext",453 "lib": ["DOM", "DOM.Iterable", "ESNext"],454 "skipLibCheck": true,455 "allowSyntheticDefaultImports": true,456 "strict": true,457 "forceConsistentCasingInFileNames": true,458 "module": "ESNext",459 "moduleResolution": "Bundler",460 "resolveJsonModule": true,461 "isolatedModules": true,462 "allowImportingTsExtensions": true,463 "noEmit": true,464 "jsx": "react-jsx"465 },466 "exclude": ["convex"],467 "include": ["**/src/**/*.tsx", "**/src/**/*.ts", "vite.config.ts"]468}469```470471#### convex/index.ts472```typescript473import {474 query,475 mutation,476 internalQuery,477 internalMutation,478 internalAction,479} from "./_generated/server";480import { v } from "convex/values";481import OpenAI from "openai";482import { internal } from "./_generated/api";483484/**485 * Create a user with a given name.486 */487export const createUser = mutation({488 args: {489 name: v.string(),490 },491 returns: v.id("users"),492 handler: async (ctx, args) => {493 return await ctx.db.insert("users", { name: args.name });494 },495});496497/**498 * Create a channel with a given name.499 */500export const createChannel = mutation({501 args: {502 name: v.string(),503 },504 returns: v.id("channels"),505 handler: async (ctx, args) => {506 return await ctx.db.insert("channels", { name: args.name });507 },508});509510/**511 * List the 10 most recent messages from a channel in descending creation order.512 */513export const listMessages = query({514 args: {515 channelId: v.id("channels"),516 },517 returns: v.array(518 v.object({519 _id: v.id("messages"),520 _creationTime: v.number(),521 channelId: v.id("channels"),522 authorId: v.optional(v.id("users")),523 content: v.string(),524 }),525 ),526 handler: async (ctx, args) => {527 const messages = await ctx.db528 .query("messages")529 .withIndex("by_channel", (q) => q.eq("channelId", args.channelId))530 .order("desc")531 .take(10);532 return messages;533 },534});535536/**537 * Send a message to a channel and schedule a response from the AI.538 */539export const sendMessage = mutation({540 args: {541 channelId: v.id("channels"),542 authorId: v.id("users"),543 content: v.string(),544 },545 returns: v.null(),546 handler: async (ctx, args) => {547 const channel = await ctx.db.get(args.channelId);548 if (!channel) {549 throw new Error("Channel not found");550 }551 const user = await ctx.db.get(args.authorId);552 if (!user) {553 throw new Error("User not found");554 }555 await ctx.db.insert("messages", {556 channelId: args.channelId,557 authorId: args.authorId,558 content: args.content,559 });560 await ctx.scheduler.runAfter(0, internal.index.generateResponse, {561 channelId: args.channelId,562 });563 return null;564 },565});566567const openai = new OpenAI();568569export const generateResponse = internalAction({570 args: {571 channelId: v.id("channels"),572 },573 returns: v.null(),574 handler: async (ctx, args) => {575 const context = await ctx.runQuery(internal.index.loadContext, {576 channelId: args.channelId,577 });578 const response = await openai.chat.completions.create({579 model: "gpt-4o",580 messages: context,581 });582 const content = response.choices[0].message.content;583 if (!content) {584 throw new Error("No content in response");585 }586 await ctx.runMutation(internal.index.writeAgentResponse, {587 channelId: args.channelId,588 content,589 });590 return null;591 },592});593594export const loadContext = internalQuery({595 args: {596 channelId: v.id("channels"),597 },598 returns: v.array(599 v.object({600 role: v.union(v.literal("user"), v.literal("assistant")),601 content: v.string(),602 }),603 ),604 handler: async (ctx, args) => {605 const channel = await ctx.db.get(args.channelId);606 if (!channel) {607 throw new Error("Channel not found");608 }609 const messages = await ctx.db610 .query("messages")611 .withIndex("by_channel", (q) => q.eq("channelId", args.channelId))612 .order("desc")613 .take(10);614615 const result = [];616 for (const message of messages) {617 if (message.authorId) {618 const user = await ctx.db.get(message.authorId);619 if (!user) {620 throw new Error("User not found");621 }622 result.push({623 role: "user" as const,624 content: `${user.name}: ${message.content}`,625 });626 } else {627 result.push({ role: "assistant" as const, content: message.content });628 }629 }630 return result;631 },632});633634export const writeAgentResponse = internalMutation({635 args: {636 channelId: v.id("channels"),637 content: v.string(),638 },639 returns: v.null(),640 handler: async (ctx, args) => {641 await ctx.db.insert("messages", {642 channelId: args.channelId,643 content: args.content,644 });645 return null;646 },647});648```649650#### convex/schema.ts651```typescript652import { defineSchema, defineTable } from "convex/server";653import { v } from "convex/values";654655export default defineSchema({656 channels: defineTable({657 name: v.string(),658 }),659660 users: defineTable({661 name: v.string(),662 }),663664 messages: defineTable({665 channelId: v.id("channels"),666 authorId: v.optional(v.id("users")),667 content: v.string(),668 }).index("by_channel", ["channelId"]),669});670```671672#### convex/tsconfig.json673```typescript674{675 /* This TypeScript project config describes the environment that676 * Convex functions run in and is used to typecheck them.677 * You can modify it, but some settings required to use Convex.678 */679 "compilerOptions": {680 /* These settings are not required by Convex and can be modified. */681 "allowJs": true,682 "strict": true,683 "moduleResolution": "Bundler",684 "jsx": "react-jsx",685 "skipLibCheck": true,686 "allowSyntheticDefaultImports": true,687688 /* These compiler options are required by Convex */689 "target": "ESNext",690 "lib": ["ES2021", "dom"],691 "forceConsistentCasingInFileNames": true,692 "module": "ESNext",693 "isolatedModules": true,694 "noEmit": true695 },696 "include": ["./**/*"],697 "exclude": ["./_generated"]698}699```700701#### src/App.tsx702```typescript703export default function App() {704 return <div>Hello World</div>;705}706```707