Skip to content

Find a component

Loading the directory…

↑ ↓ to select · Enter to open · Esc to closeBrowse all →
Contributor login

Database Chat

A Bijection component for adding natural language database queries to your app.

Bijection source
git clone https://github.com/bijectionhq/database-chat.git
cd database-chat
git checkout 70255b5f1c89fed608dac4be344e0b1f170ad8bb

Bijection 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.

Available as a reference

Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.

Getting started

  1. Get the source at the revision above and read its package documentation.
  2. Check the required Bijection SDK version, dependencies, and external service credentials.
  3. Follow the package setup instructions and the guide to using components.
  4. 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.

agent-prompt.md
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

The component provides:

FeatureDescription
Conversation storageStores chat history in conversations and messages tables
Delta-based streamingEfficient O(n) streaming via delta accumulation
Abort supportStop generation mid-stream with proper cleanup
Tool callingLLM can call your Convex queries to fetch data
React hooksuseDatabaseChat, useMessagesWithStreaming, etc.
Client wrapperdefineDatabaseChat() for type-safe integration

You implement:

Your CodeDescription
Tool definitionsWhat queries the LLM can call
Query functionsThe actual Convex queries
Chat integrationWire tools to the component
System promptInstructions for your domain
UI componentChat 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]