Database Chat
A Bijection component for adding natural language database queries to your app.
git clone https://github.com/bijectionhq/database-chat.git
cd database-chat
git checkout 70255b5f1c89fed608dac4be344e0b1f170ad8bbBijection repository access is required. Request access.
Description
A Bijection component for adding natural language database queries to your app.
Explore the package README for API concepts, configuration, and examples. Check the Bijection source for adaptation-specific setup.
Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.
Getting started
- Get the source at the revision above and read its package documentation.
- Check the required Bijection SDK version, dependencies, and external service credentials.
- Follow the package setup instructions and the guide to using components.
- Exercise the component in your deployment according to your project’s verification rules.
Open source
Maintained source: bijectionhq/database-chat. Based on dayhaysoos/convex-database-chat. License: MIT. Consult the repository for license terms, notices, and contributor attribution.
From the upstream publisher
A Convex component for adding natural language database queries to your app.
Explore the demo ↗Upstream overview
From the upstream package documentation.
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
From the upstream package documentation.
- 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
From the upstream package documentation.
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.
Use with agents and CLI
Give your coding agent the component’s source identity and setup guidance.
Help me integrate Database Chat with Bijection.
Read: https://bijection.com/components/database-chat/SKILL.md
Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.Upstream README.md
View repository ↗Documentation from dayhaysoos/convex-database-chat, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.
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
- Installation
- Quick Start
- Defining Tools
- Implementing Query Functions
- Creating the Chat Integration
- Writing System Prompts
- Building the UI
- Patterns
- API Reference
- Railway Deployments
- 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
// 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:
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
// 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
// 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:
interface Tool {
name: string; // Unique identifier
description: string; // What the tool does (LLM reads this!)
parameters: { // JSON Schema for arguments
type: "object";
properties: Record<string, {
type: "string" | "number" | "boolean" | "array" | "object";
description?: string;
enum?: string[]; // For constrained values
}>;
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
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
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
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
// 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
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]