Instruction file imported from rakeshdavid/prompthub (
.cursor/rules/convex.mdc). Copyright stays with the author.
Convex guidelines
Function guidelines
New function syntax
- ALWAYS use the new function syntax for Convex functions. For example:
typescript 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.tsand require anhttpActiondecorator. For example:typescript 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 }); }), });
Function registration
- Use
internalQuery,internalMutation, andinternalActionto 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. - Use
query,mutation, andactionto register public functions. These functions are part of the public API and are exposed to the public Internet. Do NOT usequery,mutation, oractionto register sensitive internal functions that should be kept private. - You CANNOT register a function through the
apiorinternalobjects. - ALWAYS include argument and return validators for all Convex functions. 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.runQueryto call a query from a query, mutation, or action. -
Use
ctx.runMutationto call a mutation from a mutation or action. -
Use
ctx.runActionto 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, orctx.runActionto 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
apiobject defined by the framework inconvex/_generated/api.tsto call public functions registered withquery,mutation, oraction. - Use the
internalobject defined by the framework inconvex/_generated/api.tsto call internal (or private) functions registered withinternalQuery,internalMutation, orinternalAction. - Convex uses file-based routing, so a public function defined in
convex/example.tsnamedfhas a function reference ofapi.example.f. - A private function defined in
convex/example.tsnamedghas a function reference ofinternal.example.g. - Functions can also registered within directories nested within the
convex/folder. For example, a public functionhdefined inconvex/messages/access.tshas a function reference ofapi.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, andactionto define public functions. - Use
internalQuery,internalMutation, andinternalActionto define private, internal functions.
Validator guidelines
v.bigint()is deprecated for representing signed 64-bit integers. Usev.int64()instead.- Use
v.record()for defining a record type.v.map()andv.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.
Typescript guidelines
- You can use the helper typescript type
Idimported 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 useId<'users'>to get the type of the id for that table. - If you need to define a
Recordmake sure that you correctly provide the type of the key and value in the type. For example a validatorv.record(v.id('users'), v.string())would have the typeRecord<Id<'users'>, string>. - 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 thanstring.
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
filterin queries. Instead, define an index in the schema and usewithIndexinstead. - Convex queries do NOT support
.delete(). Instead,.collect()the results, iterate over them, and callctx.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.
Ordering
- By default Convex always returns documents in ascending
_creationTimeorder. - 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.replaceto fully replace an existing document. This method will throw an error if the document does not exist. - Use
ctx.db.patchto shallow merge updates into an existing document. This method will throw an error if the document does not exist.
Scheduling guidelines
Cron guidelines
-
Only use the
crons.intervalorcrons.cronmethods to schedule cron jobs. Do NOT use thecrons.hourly,crons.daily, orcrons.weeklyhelpers. -
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
cronsobject, calling some methods on it, and then exporting it as default. For example, ```ts import { cronJobs } from "convex/server"; import { internal } from "./_generated/api";const crons = cronJobs(); // Run `internal.users.deleteInactive` every two hours. crons.interval("delete inactive users", { hours: 2 }, internal.users.deleteInactive, {}); export default crons; ``` -
You can register Convex functions within
crons.tsjust like any other file. -
If a cron calls an internal function, always import the
internalobject 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 returnsnullif the file doesn't exist. -
Do NOT use the deprecated
ctx.storage.getMetadatacall 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(args.fileId); console.log(metadata); return null; }, }); ```
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.17.4",
"openai": "^4.79.0"
}
}
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"]),
});