DatabaseChat

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.