Quick Start
Minimal end-to-end setup.
1. Define a tool
Create a Convex query that the LLM can call.
// convex/chatTools.ts
import { query } from "./_generated/server";
import { v } from "convex/values";
export const countRecords = query({
args: { table: v.string() },
returns: v.object({ count: v.number() }),
handler: async (ctx, args) => {
if (args.table === "users") {
const users = await ctx.db.query("users").collect();
return { count: users.length };
}
if (args.table === "orders") {
const orders = await ctx.db.query("orders").collect();
return { count: orders.length };
}
return { count: 0 };
},
});2. Wire the chat endpoints
Create function handles inside a Convex function, then pass your tools to the component chat action.
// convex/chat.ts
import { v } from "convex/values";
import { action, mutation, query } from "./_generated/server";
import { createFunctionHandle } from "convex/server";
import { components, api } from "./_generated/api";
import type { DatabaseChatTool } from "./components/databaseChat/tools";
async function getTools(): Promise<DatabaseChatTool[]> {
return [
{
name: "countRecords",
description: "Count records in a table. Available tables: users, orders",
parameters: {
type: "object",
properties: {
table: { type: "string", enum: ["users", "orders"] },
},
required: ["table"],
},
handler: await createFunctionHandle(api.chatTools.countRecords),
},
];
}
export const createConversation = mutation({
args: { externalId: v.string(), title: v.optional(v.string()) },
handler: async (ctx, args) => {
return await ctx.runMutation(components.databaseChat.conversations.create, {
externalId: args.externalId,
title: args.title ?? "New Chat",
});
},
});
export const listConversations = query({
args: { externalId: v.string() },
handler: async (ctx, args) => {
return await ctx.runQuery(components.databaseChat.conversations.list, {
externalId: args.externalId,
});
},
});
export const getMessages = query({
args: { conversationId: v.string() },
handler: async (ctx, args) => {
return await ctx.runQuery(components.databaseChat.messages.list, {
conversationId: args.conversationId as any,
});
},
});
export const getStreamState = query({
args: { conversationId: v.string() },
handler: async (ctx, args) => {
return await ctx.runQuery(components.databaseChat.stream.getStream, {
conversationId: args.conversationId as any,
});
},
});
export const getStreamDeltas = query({
args: { streamId: v.string(), cursor: v.number() },
handler: async (ctx, args) => {
return await ctx.runQuery(components.databaseChat.stream.listDeltas, {
streamId: args.streamId as any,
cursor: args.cursor,
});
},
});
export const abortStream = mutation({
args: { conversationId: v.string(), reason: v.optional(v.string()) },
handler: async (ctx, args) => {
return await ctx.runMutation(
components.databaseChat.stream.abortByConversation,
{
conversationId: args.conversationId as any,
reason: args.reason ?? "User cancelled",
},
);
},
});
export const sendMessage = action({
args: { conversationId: v.string(), message: v.string() },
handler: async (ctx, args) => {
const tools = await getTools();
return await ctx.runAction(components.databaseChat.chat.send, {
conversationId: args.conversationId,
message: args.message,
config: {
apiKey: process.env.OPENROUTER_API_KEY!,
systemPrompt:
"You are a helpful assistant. Use the available tools to answer questions about the database.",
tools,
},
});
},
});Raw tools keep working. For count/list/semantic tools that return the standard
{ data, meta } result contract, see Tools for typed
builders that attach reliability metadata and enable automatic result guidance.
3. Add a minimal UI
import { useEffect, useState } from "react";
import { useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
import {
DatabaseChatProvider,
useDatabaseChat,
} from "@dayhaysoos/convex-database-chat";
function ChatWidget() {
const [conversationId, setConversationId] = useState<string | null>(null);
const createConversation = useMutation(api.chat.createConversation);
useEffect(() => {
createConversation({ externalId: "user:demo" }).then(setConversationId);
}, [createConversation]);
const { messages, streamingContent, isStreaming, isLoading, send, abort } =
useDatabaseChat({ conversationId });
return (
<div>
{messages?.map((msg) => (
<div key={msg._id}>{msg.content}</div>
))}
{streamingContent && <div>{streamingContent}</div>}
<button
onClick={() => conversationId && send("How many users do we have?")}
disabled={!conversationId || isLoading || isStreaming}
>
Ask
</button>
{isStreaming && <button onClick={abort}>Stop</button>}
</div>
);
}
export default function App() {
return (
<DatabaseChatProvider
api={{
getMessages: api.chat.getMessages,
listConversations: api.chat.listConversations,
getStreamState: api.chat.getStreamState,
getStreamDeltas: api.chat.getStreamDeltas,
createConversation: api.chat.createConversation,
abortStream: api.chat.abortStream,
sendMessage: api.chat.sendMessage,
}}
>
<ChatWidget />
</DatabaseChatProvider>
);
}Replace externalId with your own auth identifier. For richer UI examples, see
the React Hooks guide.