# A minimal chatbot template built with Next.js

> Source: <https://github.com/shadcn-ui/chatbot-template>
> Published: 2026-08-12 08:14:08+00:00

A minimal chatbot template built with Next.js, the [AI SDK](https://ai-sdk.dev), [shadcn/ui](https://ui.shadcn.com), [shadcn/react](https://ui.shadcn.com/docs/react/message-scroller), [shadcn/typeset](https://ui.shadcn.com/docs/typeset) and the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway).

- Streaming chat with markdown rendering and shadcn/typeset
- Tool calling example
- Web search via each provider's built-in search tool
- Human-in-the-loop questionnaire. The model can ask clarifying questions, answered with the shadcn questionnaire component

That's it — no configuration needed. Vercel deployments authenticate to the AI Gateway automatically via OIDC, and usage runs on your team's [AI Gateway credits](https://vercel.com/docs/ai-gateway/pricing).

```
pnpm install
```

Then give the app a gateway credential, either by pulling an OIDC token from your linked Vercel project:

```
vercel link
vercel env pull
```

or by creating an API key in the Vercel dashboard (**AI Gateway → API Keys**) and adding it to `.env.local`

:

```
cp .env.example .env.local
# then set AI_GATEWAY_API_KEY=...
```

Start the dev server:

```
pnpm dev
```

| Env var | Required | Description |
|---|---|---|
`AI_GATEWAY_API_KEY` |
Local dev only | AI Gateway API key. Not needed on Vercel deployments (OIDC). |

The model list lives in [lib/models.ts](/shadcn-ui/chatbot-template/blob/main/lib/models.ts) — the first entry is the default model.

The `/api/chat`

route is **public and unauthenticated** — every request spends your AI Gateway credits. That's fine for a personal demo, but before putting it in front of real traffic you should:

**Rate limit it.** Add[Vercel Firewall / WAF](https://vercel.com/docs/security/vercel-waf)rules orso a single client can't drain your credits (denial-of-wallet).`@upstash/ratelimit`

**Cap spend.** Set an[AI Gateway spend limit](https://vercel.com/docs/ai-gateway/observability-and-spend/budgets)as a backstop.**Add auth** if the chatbot isn't meant to be public.

The route already validates the request body, restricts models to [lib/models.ts](/shadcn-ui/chatbot-template/blob/main/lib/models.ts), caps output tokens and step count, and aborts generation on client disconnect — but those bound a single request, not overall volume.

[app/api/chat/route.ts](/shadcn-ui/chatbot-template/blob/main/app/api/chat/route.ts)streams responses with`streamText`

[components/chat.tsx](/shadcn-ui/chatbot-template/blob/main/components/chat.tsx)renders the conversation with`useChat`

and shadcn chat primitives.[tools/](/shadcn-ui/chatbot-template/blob/main/tools)defines the tools — one file per tool (the filename is the model-facing tool name), composed in[tools/index.ts](/shadcn-ui/chatbot-template/blob/main/tools/index.ts): a server-executed GitHub repo lookup, the interactive`ask_user`

questionnaire, and provider-native web search.

Assistant messages are a list of typed parts. [components/chat-message.tsx](/shadcn-ui/chatbot-template/blob/main/components/chat-message.tsx) switches on `part.type`

and delegates each one to a component in [components/parts/](/shadcn-ui/chatbot-template/blob/main/components/parts):

| Part type | Component | Renders |
|---|---|---|
`text` |
|

`tool-github_repo`

[github-repo-part.tsx](/shadcn-ui/chatbot-template/blob/main/components/parts/github-repo-part.tsx)`tool-web_search`

[web-search-part.tsx](/shadcn-ui/chatbot-template/blob/main/components/parts/web-search-part.tsx)`tool-ask_user`

[ask-user-part.tsx](/shadcn-ui/chatbot-template/blob/main/components/parts/ask-user-part.tsx)[question-card.tsx](/shadcn-ui/chatbot-template/blob/main/components/question-card.tsx), pinned to the scroller bottom.`source-url`

[sources-part.tsx](/shadcn-ui/chatbot-template/blob/main/components/parts/sources-part.tsx)Tool parts move through states as the stream progresses — `input-streaming`

→ `input-available`

→ `output-available`

(or `output-error`

) — and each component switches on `part.state`

to show progress, results, and failures.

- Create
`tools/<name>.ts`

(the filename is the model-facing tool name) exporting a`tool()`

with a`description`

, an`inputSchema`

, and an`execute`

function (omit`execute`

for tools the user answers in the UI, like`ask_user`

), then register it in[tools/index.ts](/shadcn-ui/chatbot-template/blob/main/tools/index.ts). - Add a part component in
[components/parts/](/shadcn-ui/chatbot-template/blob/main/components/parts)and a`case "tool-<name>"`

in[chat-message.tsx](/shadcn-ui/chatbot-template/blob/main/components/chat-message.tsx).

Message types are inferred from the tool definitions via `InferUITools`

, so `part.input`

and `part.output`

are fully typed in your part component — renaming a tool field is a build error, not a silent `undefined`

.

```
npx shadcn@latest add button
```

MIT — see [LICENSE](/shadcn-ui/chatbot-template/blob/main/LICENSE).
