DatabaseChat

Quick Start

Minimal end-to-end setup using the client wrapper.

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

Use defineDatabaseChat to create a typed wrapper around the component.

// 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 { defineDatabaseChat } from "./components/databaseChat/client";
import type { DatabaseChatTool } from "./components/databaseChat/tools";

const tools: DatabaseChatTool[] = [
  {
    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: createFunctionHandle(api.chatTools.countRecords),
  },
];

const chat = defineDatabaseChat(components.databaseChat, {
  tools,
  systemPrompt:
    "You are a helpful assistant. Use the available tools to answer questions about the database.",
});

export const createConversation = mutation({
  args: { externalId: v.string(), title: v.optional(v.string()) },
  handler: async (ctx, args) => chat.createConversation(ctx, args),
});

export const listConversations = query({
  args: { externalId: v.string() },
  handler: async (ctx, args) => chat.listConversations(ctx, args.externalId),
});

export const getMessages = query({
  args: { conversationId: v.string() },
  handler: async (ctx, args) => chat.getMessages(ctx, args.conversationId),
});

export const getStreamState = query({
  args: { conversationId: v.string() },
  handler: async (ctx, args) => chat.getStreamState(ctx, args.conversationId),
});

export const getStreamDeltas = query({
  args: { streamId: v.string(), cursor: v.number() },
  handler: async (ctx, args) => chat.getStreamDeltas(ctx, args.streamId, args.cursor),
});

export const abortStream = mutation({
  args: { conversationId: v.string(), reason: v.optional(v.string()) },
  handler: async (ctx, args) =>
    chat.abortStream(ctx, args.conversationId, args.reason ?? "User cancelled"),
});

export const sendMessage = action({
  args: { conversationId: v.string(), message: v.string() },
  handler: async (ctx, args) =>
    chat.send(ctx, {
      conversationId: args.conversationId,
      message: args.message,
      apiKey: process.env.OPENROUTER_API_KEY!,
    }),
});

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.