# Convex Examples Reference implementations for common Convex patterns. --- ## Example: Chat App with AI Responses A real-time chat backend demonstrating: - User and channel management - Message storage with proper ordering - OpenAI integration for AI responses - Background job scheduling ### Task Requirements - 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 (10 most recent per channel) ### API Design **Public Mutations:** - `createUser` - Create user with name - `createChannel` - Create channel with name - `sendMessage` - Send message and trigger AI response **Public Queries:** - `listMessages` - Get 10 most recent messages (descending) **Internal Functions:** - `generateResponse` - Call OpenAI API - `loadContext` - Load message history for AI context - `writeAgentResponse` - Save AI response to database ### Schema Design ``` users: { name: string } channels: { name: string } messages: { channelId, authorId?, content } + index by_channel ``` --- ### Implementation #### package.json ```json { "name": "chat-app", "version": "1.0.0", "dependencies": { "convex": "^1.31.2", "openai": "^4.79.0" }, "devDependencies": { "typescript": "^5.7.3" } } ``` #### convex/schema.ts ```typescript 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/index.ts ```typescript 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/tsconfig.json ```json { "compilerOptions": { "allowJs": true, "strict": true, "moduleResolution": "Bundler", "jsx": "react-jsx", "skipLibCheck": true, "allowSyntheticDefaultImports": true, "target": "ESNext", "lib": ["ES2021", "dom"], "forceConsistentCasingInFileNames": true, "module": "ESNext", "isolatedModules": true, "noEmit": true }, "include": ["./**/*"], "exclude": ["./_generated"] } ``` #### tsconfig.json (root) ```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"] } ```