Skip to Content
TutorialsAI Series1. Chat With Your Data

Tutorial 1: Chat With Your Data

In this tutorial you’ll add a /chat page to your generated app where users ask natural-language questions about their data. Large Language Model (LLM) — hosted on AWS Bedrock — decides when to call your DaaS REST API, fetches the right rows, and streams the answer back. Tool calls are rendered inline so users see exactly which collections were queried.

By the end you’ll have:

  • A /api/chat route in your app that streams from AWS Bedrock
  • Two tools (list_collections, query_collection) that the model can invoke to read your DaaS data
  • A /chat page built with the starter’s existing Mantine components
  • A working end-to-end demo against your own collections

Prerequisites

This tutorial adds a new /api/chat route inside your generated app.

Enable a Bedrock model in your region

In the AWS console, open Bedrock → Model access and request access to Amazon Nova Lite — a fast, cheap model that’s great for tool-calling. Use the cross-region inference profile that matches the region you’ll call from:

RegionSuggested model id
us-east-1, us-west-2us.amazon.nova-lite-v1:0
eu-*eu.amazon.nova-lite-v1:0
ap-southeast-1, ap-southeast-2, ap-northeast-1apac.amazon.nova-lite-v1:0

After granting model access, also confirm the inference profile is enabled for your account under Bedrock → Cross-region inference. If it isn’t, your first request returns a 403.

Generate a Bedrock API key in the console (Bedrock → API keys) and add the following to your app’s .env.local:

AWS_REGION=ap-southeast-1 AWS_BEARER_TOKEN_BEDROCK=bedrock-api-key-xxxx BEDROCK_MODEL_ID=apac.amazon.nova-lite-v1:0

Confirm dependencies are present

The Buildpad starter already ships with everything you need:

  • ai + @ai-sdk/amazon-bedrock — server-side streaming and the Bedrock provider
  • @ai-sdk/react — the useChat hook for the client
  • zod — input schemas for tool definitions
  • @mantine/core + @tabler/icons-react — the UI primitives you’ll build the chat page with

Verify them with:

pnpm list ai @ai-sdk/amazon-bedrock @ai-sdk/react zod @mantine/core

If any are missing, install them:

pnpm add ai @ai-sdk/amazon-bedrock @ai-sdk/react zod

Create the chat API route

Create app/api/chat/route.ts in your app:

import { bedrock } from "@ai-sdk/amazon-bedrock"; import { convertToModelMessages, stepCountIs, streamText } from "ai"; import { dataTools } from "./tools"; export const runtime = "nodejs"; export const maxDuration = 60; export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: bedrock(process.env.BEDROCK_MODEL_ID!), system: "You are an assistant embedded in a Buildpad app. " + "Use the provided tools to answer questions about the user's collections. " + "Always call list_collections first if you don't already know what data exists.", messages: await convertToModelMessages(messages), tools: dataTools, stopWhen: stepCountIs(5), }); return result.toUIMessageStreamResponse(); }

stopWhen: stepCountIs(5) lets the model chain tool calls (e.g. discover collections, then query one) within a single user turn. convertToModelMessages (async — note the await) converts the UI-shaped messages from useChat into the model-shaped messages streamText expects.

Define the data-access tools

Create app/api/chat/tools.ts:

import { tool } from "ai"; import { z } from "zod"; import { getAuthHeaders, getDaasUrl } from "@/lib/api/auth-headers"; async function daas(path: string) { const url = `${getDaasUrl()}${path}`; const headers = await getAuthHeaders(); const res = await fetch(url, { headers, cache: "no-store" }); if (!res.ok) throw new Error(`DaaS ${res.status}: ${await res.text()}`); return res.json(); } export const dataTools = { list_collections: tool({ description: "List all available collections in this app, with their field names and types.", inputSchema: z.object({}), execute: async () => { const [{ data: collections }, { data: fields }] = await Promise.all([ daas("/api/collections"), daas("/api/fields"), ]); return (collections as any[]).map((c: any) => ({ collection: c.collection, fields: (fields as any[]) .filter((f: any) => f.collection === c.collection) .map((f: any) => ({ field: f.field, type: f.type })), })); }, }), query_collection: tool({ description: "Fetch rows from a collection. Use this after list_collections to answer questions about the user's data.", inputSchema: z.object({ collection: z.string().describe("The collection slug, e.g. 'tasks'."), limit: z.number().int().min(1).max(50).default(10), sort: z .string() .optional() .describe("Field to sort by. Prefix with '-' for descending."), }), execute: async ({ collection, limit, sort }) => { const params = new URLSearchParams({ limit: String(limit) }); if (sort) params.set("sort", sort); const { data } = await daas(`/api/items/${collection}?${params}`); return data; }, }), };

Two things to call out:

  • inputSchema: is the v6 name (older docs you might find online say parameters: — those are pre-v5)
  • getAuthHeaders / getDaasUrl come from the Buildpad starter at lib/api/auth-headers.ts. They reuse the Supabase session JWT, so every tool call respects the calling user’s DaaS permissions via RBAC — no extra service tokens to manage.

These two tools cover the common case. The full DaaS operation set — filtering, relations, schema introspection, writes — is documented in the MCP tool reference . Add more tool({...}) entries here as your app grows.

Build the chat page

Create app/(authenticated)/chat/page.tsx using Mantine primitives:

"use client"; import { Box, Button, Code, Group, Paper, ScrollArea, Stack, Text, Textarea, Title, } from "@mantine/core"; import { useChat } from "@ai-sdk/react"; import { useState } from "react"; export default function ChatPage() { const { messages, sendMessage, status } = useChat(); const [input, setInput] = useState(""); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!input.trim() || status !== "ready") return; sendMessage({ text: input }); setInput(""); }; return ( <Stack h="calc(100vh - 4rem)" gap="md" p="md" maw={800} mx="auto"> <Title order={3}>Chat with your data</Title> <ScrollArea style={{ flex: 1 }} type="auto"> <Stack gap="md"> {messages.map((m) => ( <Paper key={m.id} p="sm" withBorder radius="md"> <Text fw={600} size="sm" mb={4} c={m.role === "user" ? "blue" : "grape"} > {m.role === "user" ? "You" : "Assistant"} </Text> <Stack gap="xs"> {m.parts.map((part, i) => { if (part.type === "text") { return ( <Text key={i} style={{ whiteSpace: "pre-wrap" }}> {part.text} </Text> ); } if (part.type.startsWith("tool-")) { const p = part as any; return ( <Box key={i}> <Text size="xs" c="dimmed" mb={4}> {p.type.replace("tool-", "")} — {p.state} </Text> {p.output !== undefined && ( <Code block style={{ maxHeight: 240, overflow: "auto" }} > {JSON.stringify(p.output, null, 2)} </Code> )} </Box> ); } return null; })} </Stack> </Paper> ))} </Stack> </ScrollArea> <form onSubmit={handleSubmit}> <Group align="flex-end" gap="sm"> <Textarea value={input} onChange={(e) => setInput(e.currentTarget.value)} placeholder="Ask about your data…" autosize minRows={1} maxRows={6} style={{ flex: 1 }} disabled={status !== "ready"} /> <Button type="submit" disabled={status !== "ready" || !input.trim()}> Send </Button> </Group> </form> </Stack> ); }

What’s happening:

  • useChat() (no api argument) uses the default transport, which posts to /api/chat
  • messages[i].parts is an array of typed parts — type: "text" for streamed text, type: "tool-<toolName>" for tool invocations with state ("input-streaming" | "input-available" | "output-available" | "output-error") and output
  • sendMessage({ text }) is the v6 replacement for the old handleSubmit + managed input API

Placing the page under (authenticated) ensures it inherits DaaSProviderWrapper from the starter’s app/(authenticated)/layout.tsx. The DaaS token used by the tools lives server-side and is never sent to the browser.

Add a link to /chat from your existing authenticated landing page — for example, in app/(authenticated)/page.tsx, drop in a Mantine <Anchor component={Link} href="/chat">Open chat</Anchor>. Match whatever nav pattern your generated app already uses.

Verify It Works

With pnpm dev running:

Open the chat page

Navigate to http://localhost:3000/chat. You should see an empty conversation with a prompt input at the bottom.

Empty chat page showing the conversation area and prompt input at the bottom

Ask what data exists

Type “What collections do I have?” and submit. Expected:

  • The assistant’s first turn calls list_collections (rendered in a tool panel under the message)
  • A streamed reply lists the collections with their fields

Query a collection

Type “Show me the 5 most recent rows in <your-collection>.” Expected:

  • The assistant calls query_collection with limit: 5 and a - prefixed sort on a timestamp field
  • The tool output panel shows the JSON returned by your DaaS instance
  • The streamed reply summarises the rows in natural language

Chat page showing the query_collection tool panel with JSON output and an AI-written summary of the most recent rows

If a tool call errors with a 401, make sure you’re signed in to the app (the tools use your Supabase session JWT) and that BUILDPAD_DAAS_URL (or NEXT_PUBLIC_BUILDPAD_DAAS_URL) is set in .env.local. If Bedrock returns a 403, re-check that the inference profile for BEDROCK_MODEL_ID is enabled under Bedrock → Cross-region inference in your account.

This is a manual UI verification — adding Playwright coverage for streamed chat responses is left as a follow-up. The same pattern as Tutorial 8: Event Hooks & Extensions applies if you want to add it now.

What’s Next

Your app now has a chat surface backed by real data. Continue the series to add more AI features:

  • Tutorial 2: AI-Assisted Form Fill — let users describe what they want to create in plain English; the AI drafts the form fields and the user reviews before saving
  • Tutorial 3: AI Summaries & Reports — add a Generate Summary button that fetches live collection data via tool calling and streams a formatted report into a Mantine card
Last updated on