# Database Chat A Bijection component for adding natural language database queries to your app. - Package: @bijectionhq/database-chat - Category: AI & Agents - Status: Reference - License: MIT - Bijection source (repository access required): https://github.com/bijectionhq/database-chat/tree/70255b5f1c89fed608dac4be344e0b1f170ad8bb - Upstream: https://github.com/dayhaysoos/convex-database-chat/tree/627dc0cf8be0ec73ea6d85804e3f83c3c40c77aa - Documentation: https://docs.bijection.com/components/using - Directory: https://bijection.com/components/database-chat ## Get the source Bijection repository access is required. Request access at https://bijection.com/contact. ```sh git clone https://github.com/bijectionhq/database-chat.git cd database-chat git checkout 70255b5f1c89fed608dac4be344e0b1f170ad8bb ``` ## Before using this component Read the package README at the recorded revision for setup, dependencies, and API examples. Upstream documentation describes the original package and may differ from the Bijection adaptation. This is a source candidate for evaluation. Catalog inclusion does not establish npm availability or runtime qualification. Check compatibility with your SDK and deployment before adoption. External providers run their own compute. ## Upstream package documentation Recorded 2026-10-23. This describes @dayhaysoos/convex-database-chat; check the Bijection adaptation separately. DatabaseChat is a Convex component that lets users query your database using natural language. It provides conversation storage, efficient delta-based streaming, and tool calling capabilities that allow an LLM to execute your Convex queries based on user questions. You define query tools, implement the corresponding Convex functions, and the component handles the chat flow and streaming responses. ### Use cases • **Customer support dashboards** - Let support agents ask "show me all pending orders from last week" instead of writing database queries • **Business intelligence tools** - Enable stakeholders to explore data with questions like "what's our revenue breakdown by category this month" • **Admin panels** - Allow non-technical team members to query user data, product inventory, or analytics without SQL knowledge • **Data exploration interfaces** - Build internal tools where analysts can ask ad-hoc questions about your application data • **Self-service reporting** - Create customer-facing interfaces where users can query their own account data or usage statistics ### How it works You define tools that describe what database operations the LLM can perform, specifying parameters using JSON Schema and mapping them to Convex query functions. The component uses OpenRouter's API to process natural language requests and determine which tools to call with what arguments. The component stores conversations in `conversations` and `messages` tables, and implements delta-based streaming for efficient real-time updates. Instead of sending full accumulated responses on each update, it streams only new content deltas and provides `useMessagesWithStreaming` and other React hooks for the client. You integrate it by calling `components.databaseChat.chat.send` from your actions, passing the tools array and system prompt. The component handles tool execution, response generation, and stream management. You can abort streams mid-generation and the component provides queries like `getStreamState` and `getStreamDeltas` for building responsive chat UIs. ### Upstream README # DatabaseChat Component > **Alpha Release** - A Convex component for adding natural language database queries to your app. DatabaseChat lets users ask questions about your data in plain English. The LLM calls tools you define to query your database and returns helpful, actionable responses. ## Table of Contents - [What It Does](#what-it-does) - [Installation](#installation) - [Quick Start](#quick-start) - [Defining Tools](#defining-tools) - [Implementing Query Functions](#implementing-query-functions) - [Creating the Chat Integration](#creating-the-chat-integration) - [Writing System Prompts](#writing-system-prompts) - [Building the UI](#building-the-ui) - [Patterns](#patterns) - [API Reference](#api-reference) - [Railway Deployments](#railway-deployments) - [Testing](#testing) --- ## What It Does The component provides: | Feature | Description | |---------|-------------| | **Conversation storage** | Stores chat history in `conversations` and `messages` tables | | **Delta-based streaming** | Efficient O(n) streaming via delta accumulation | | **Abort support** | Stop generation mid-stream with proper cleanup | | **Tool calling** | LLM can call your Convex queries to fetch data | | **React hooks** | `useDatabaseChat`, `useMessagesWithStreaming`, etc. | | **Client wrapper** | `defineDatabaseChat()` for type-safe integration | **You implement:** | Your Code | Description | |-----------|-------------| | **Tool definitions** | What queries the LLM can call | | **Query functions** | The actual Convex queries | | **Chat integration** | Wire tools to the component | | **System prompt** | Instructions for your domain | | **UI component** | Chat interface (or use the hooks) | ### How Delta-Based Streaming Works Traditional streaming sends the full accumulated content on every update, resulting in O(n²) bandwidth: - Update 1: "H" (1 byte) - Update 2: "He" (2 bytes) - Update 3: "Hel" (3 bytes) - ...for a 1000 character response, you send ~500,000 bytes total Delta-based streaming sends only new content, resulting in O(n) bandwidth: - Delta 1: "H" (1 byte) - Delta 2: "e" (1 byte) - Delta 3: "l" (1 byte) - ...for a 1000 character response, you send ~1000 bytes total The client accumulates deltas locally and the server cleans them up when the stream finishes. --- ## Installation ### 1. Add the component to your app ```typescript // convex/convex.config.ts import { defineApp } from "convex/server"; import databaseChat from "./components/databaseChat/convex.config"; const app = defineApp(); app.use(databaseChat); export default app; ``` ### 2. Set up OpenRouter API key Add `OPENROUTER_API_KEY` to your Convex environment variables: ```bash npx convex env set OPENROUTER_API_KEY your_key_here ``` --- ## Quick Start Here's the minimum to get a working chat: ### 1. Create a simple query tool ```typescript // 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. Create the chat integration ```typescript // convex/chat.ts import { v } from "convex/values"; import { action, mutation, query } from "./_generated/server"; import { components, api } from "./_generated/api"; const SYSTEM_PROMPT = `You are a helpful assistant. Use the available tools to answer questions about the database.`; const TOOLS = [ { 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: "countRecords", }, ]; // Create conversation export const createConversation = mutation({ args: { title: v.optional(v.string()) }, returns: v.string(), handler: async (ctx, args) => { return await ctx.runMutation(components.databaseChat.conversations.create, { externalId: "user-id", // Use your auth system title: args.title ?? "New Chat", }); }, }); // Get messages export const getMessages = query({ args: { conversationId: v.string() }, returns: v.array(v.any()), handler: async (ctx, args) => { return await ctx.runQuery(components.databaseChat.messages.list, { conversationId: args.conversationId as any, }); }, }); // Get stream state (for real-time UI updates) 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, }); }, }); // Get stream deltas (efficient O(n) streaming) 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, }); }, }); // Abort a stream (stop generation) 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", }); }, }); // Send message (see full implementation in "Creating the Chat Integration") export const sendMessage = action({ args: { conversationId: v.string(), message: v.string() }, handler: async (ctx, args) => { // Implementation shown below }, }); ``` --- ## Defining Tools Tools tell the LLM what queries it can call. Each tool needs: ```typescript interface Tool { name: string; // Unique identifier description: string; // What the tool does (LLM reads this!) parameters: { // JSON Schema for arguments type: "object"; properties: Record; required?: string[]; }; handlerType?: "query" | "mutation" | "action"; // Default: "query" handler: string; // Function handle string (createFunctionHandle) } ``` Note: When passing tools to the component chat action, `handler` should be a function handle created via `createFunctionHandle(...)`. Set `handlerType` to "action" for tools that run `ctx.vectorSearch`. ### Example: E-commerce - Search products ```typescript const searchProductsTool = { name: "searchProducts", description: "Search products by name, category, or price range", parameters: { type: "object", properties: { query: { type: "string", description: "Search text to match against product name", }, category: { type: "string", description: "Filter by category", enum: ["electronics", "clothing", "home", "sports"], }, minPrice: { type: "number", description: "Minimum price", }, maxPrice: { type: "number", description: "Maximum price", }, limit: { type: "number", description: "Maximum results (default: 20)", }, }, required: [], }, handler: "searchProducts", }; ``` ### Example: Project management - Get tasks ```typescript const getTasksTool = { name: "getTasks", description: "Get tasks with optional filters for status, assignee, or project", parameters: { type: "object", properties: { status: { type: "string", description: "Filter by task status", enum: ["todo", "in_progress", "review", "done"], }, assigneeId: { type: "string", description: "Filter by assigned user ID", }, projectId: { type: "string", description: "Filter by project ID", }, priority: { type: "string", enum: ["low", "medium", "high", "urgent"], }, }, required: [], }, handler: "getTasks", }; ``` ### Example: Content platform - Search articles ```typescript const searchArticlesTool = { name: "searchArticles", description: "Search published articles by title, author, or tags", parameters: { type: "object", properties: { query: { type: "string", description: "Search text for title or content", }, authorId: { type: "string", description: "Filter by author", }, tags: { type: "array", description: "Filter by tags", items: { type: "string" }, }, publishedAfter: { type: "string", description: "ISO date string - only articles after this date", }, }, required: [], }, handler: "searchArticles", }; ``` ### Tips for tool descriptions - **Be specific**: "Search products by name, category, or price" is better than "Search products" - **List available options**: "Available categories: electronics, clothing, home, sports" - **Explain what each parameter does**: The LLM uses descriptions to decide which to use --- ## Implementing Query Functions Your query functions do the actual database work. They receive the arguments the LLM provides. ### Example: E-commerce - Search with filters ```typescript // convex/chatTools.ts import { query } from "./_generated/server"; import { v } from "convex/values"; export const searchProducts = query({ args: { query: v.optional(v.string()), category: v.optional(v.string()), minPrice: v.optional(v.number()), maxPrice: v.optional(v.number()), limit: v.optional(v.number()), }, returns: v.array( v.object({ id: v.string(), name: v.string(), category: v.string(), price: v.number(), inStock: v.boolean(), viewUrl: v.string(), }) ), handler: async (ctx, args) => { const limit = Math.min(args.limit ?? 20, 100); let products = await ctx.db.query("products").collect(); // Apply filters if (args.category) { products = products.filter((p) => p.category === args.category); } if (args.minPrice !== undefined) { products = products.filter((p) => p.price >= args.minPrice!); } if (args.maxPrice !== undefined) { products = products.filter((p) => p.price <= args.maxPrice!); } if (args.query) { const q = args.query.toLowerCase(); products = products.filter((p) => p.name.toLowerCase().includes(q)); } return products.slice(0, limit).map((p) => ({ id: p._id, name: p.name, category: p.category, price: p.price, inStock: p.stockCount > 0, viewUrl: `/products/${p._id}`, })); }, }); ``` ### Example: Aggregation query ```typescript export const getOrderStats = query({ args: { period: v.optional(v.string()) }, returns: v.object({ totalOrders: v.number(), totalRevenue: v.number(), averageOrderValue: v.number(), statusBreakdown: v.object({ pending: v.number(), processing: v.number(), shipped: v.number(), delivered: v.number(), }), }), handler: async (ctx, args) => { let orders = await ctx.db.query("orders").collect(); // Filter by period if provided if (args.period === "today") { const today = new Date().setHours(0, 0, 0, 0); orders = orders.filter((o) => o.createdAt >= today); } else if (args.period === "week") { const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000; orders = orders.filter((o) => o.createdA [README truncated for prompt length]