The template ships its own cost-exhaustion warning, a one-variable configuration, and a tool name that is a filename
A minimal chatbot template built with Next.js, AI SDK, shadcn/ui, shadcn/react, shadcn/typeset. It runs on the Vercel AI Gateway.
At a glance
- What is it?
- A minimal Next.js chatbot template with streaming, tool calls, provider-native web search and a human-in-the-loop questionnaire, running on a hosted model gateway. The security note is the most valuable text on the page.
- Who is it for?
- This is the rare template that tells you its own security posture before you deploy it, and that makes it a better starting point than most. The streaming route is public and unauthenticated by design, every request spends gateway credits, and the page names the failure mode in three words: denial of wallet.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 30 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.
Star history
- shadcn-ui/chatbot-template1,207 ★ · +262
| Repository | 2026-09-21 | 2026-11-13 |
|---|---|---|
| shadcn-ui/chatbot-template | 945 | 1.2k |
262 Stars in the last 53 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
The template documents its own cost-exhaustion hole by name
The security section is the most valuable paragraph in the repository and it is worth reading in full.
The chat route is public and unauthenticated, and every request spends the deployer's gateway credits. The page says that is fine for a personal demo and then names the risk in plain terms as denial of wallet, so that a single client can drain the balance.
Three remedies are listed, in order of importance. Rate limit the route, using either the hosting provider's firewall rules or a rate-limiting library, so one client cannot spend your credits. Cap spend, as a backstop, through a budget on the gateway itself. And add authentication if the chatbot is not meant to be public.
Then the part that distinguishes a careful template from a careless one. The route already validates the request body, restricts models to one source file, caps output tokens and caps the step count, and aborts generation when the client disconnects. Those four guards are real. The page then says what they do not do: they bound a single request, not overall volume.
So the template ships with a named vulnerability class, the per-request mitigations already in place, and an explicit statement that the missing control is the aggregate one.
One small catch: the rate-limiting library the page recommends is not in the dependency list, so the first remedy is an install the template does not perform.
The whole configuration surface is one variable and the code speaks to three providers
There is one configuration table and it has one row.
The variable is the gateway API key, and the required column says local development only, with a note that it is not needed on a deployment because that path authenticates through the platform's own token exchange. The example environment file contains the same single line with an empty value.
The deployment path is genuinely zero configuration. The page says so directly: clone the template on the hosting platform and it works, because the deployment authenticates to the gateway automatically and usage is billed to the team that owns the project.
The local path is where it gets less zero-configuration than the framing suggests. There are two routes and both involve the platform. One links the local checkout to a project and pulls its environment; the other asks you to create a key in the platform's dashboard and copy it into a local environment file. There is no third route, so a developer working entirely offline from the platform's tooling is not a supported case.
Now put that next to the dependency list. Three separate provider packages are installed, one each for the gateway, for one model vendor and for another. So the code is written to address three providers while the documented configuration wires exactly one of them, and nothing in the page explains what the other two are for or how you would authenticate to them.
The default model is whichever entry is first in one file
There is no environment variable for the model. The page says the model list lives in a single source file and that the first entry is the default.
So the default model is an array ordering, and changing it is a code change. That is a reasonable choice for a template whose whole point is to show the shape of the code, and it has the side effect of making the model choice invisible to anyone reading the environment configuration.
The same file appears in two roles, and that is the part with teeth. The configuration section says the list lives there; the security section says the route restricts models to that same file.
One file is therefore both the selector and the allowlist. The consequence is that the list is a security control: a model added to it becomes reachable from the public route, and a model removed from it becomes unreachable, with no second place to update. That is a good property, and it also means a model added for experimentation is added to production reach at the same moment, because there is only one list and no environment-specific override.
The per-request caps the page mentions, on output tokens and on step count, sit on the same code path, so the model, its limits and its reachability are all configured in the same place.
A tool's name is its filename, and one kind of rename fails the build while the other does not
The tool convention is stated twice and it is unusual enough to matter.
Tools live in a directory, one file per tool, and the filename is the model-facing tool name. Three tools ship: a server-executed repository lookup for a code host, an interactive questionnaire tool, and provider-native web search.
Adding one takes four edits, and the page enumerates them. Create a file under the tools directory whose name is the tool's name, exporting a tool definition with a description, an input schema and an execute function, with the explicit instruction to omit the execute function for tools the user answers in the interface. Register it in the tools index. Add a component under the parts directory. Add a case to a switch in the message renderer.
Then the sentence that justifies the whole design. Message types are inferred from the tool definitions, so the input and output on each part are fully typed in your component, and renaming a tool field is a build error rather than a silent undefined.
Now follow the consequence. Renaming a field is caught at build time. Renaming a file changes the name the model sees, and the renderer matches on a plain string case, so nothing catches it. The tool keeps working, the model calls a name the switch does not have, and the part simply does not render. One rename fails loudly and the other fails silently, in the same convention.
Five part types, four of them tools and one that is not
Assistant messages are lists of typed parts, and the renderer switches on the part type to delegate to a component. The table lists five.
Plain text renders as markdown through a markdown component and a typography package. The repository tool gets a spinner while the lookup runs and then a linked line of statistics: stars, forks and language. The web search tool gets a searching status and then a persistent line per search saying it searched. The questionnaire tool renders answered questions inline, with pending questions in a separate card pinned to the bottom of the scroller. And a fifth entry, a source type, is web search citations, deduplicated into a drawer summarising how many sites were searched once the message finishes streaming.
That fifth entry is the one structural oddity: a citation is not a tool, and the table is a list of tool parts. It is there because the renderer switches on the same discriminator, so citations and tool results travel through one union type. That is a reasonable design and a slightly misleading label.
The parts also move through states as the stream progresses, from input streaming to input available to output available or an error state, and each component switches on the state as well as the type. So adding a tool means implementing two axes, which is the part the four-step instruction leaves implicit.
The deduped citation drawer is the most product-minded piece here, since it is the one thing that would be tedious to add later.
A two-character class-name utility is listed as a runtime package
The dependency list has nineteen runtime entries and one of them is two characters long: a class-name concatenation helper, pinned with a caret range.
That helper is, in every project built on this component approach, a small local file in a utilities module. There is a source directory in this repository's tree. So as written, a fresh install would try to resolve a package from the public registry that is almost certainly not meant to be there, and the version range means nobody would notice immediately.
That is the sort of thing a template invites, because templates are cloned and installed rather than read. It is also a five-second fix, and mentioning it here is not an accusation so much as a note to anyone about to run the install.
Two other observations from the same list. The framework and the two React packages are pinned to exact versions with no range, while all sixteen other runtime entries use caret ranges. So the versions most likely to matter for a framework upgrade are the ones held still, and the ones held loosely are the ones that float.
A syntax highlighter is installed as a runtime dependency and is not mentioned anywhere in the page, which describes markdown rendering as markdown plus a typography package. And there is a types package for Node pinned to an older major than anything else in the file, with no engine constraint declared for Node at all.
The lint script names no files and the format script globs two extensions
The two quality scripts are written in visibly different styles.
"format": "prettier --write \"**/*.{ts,tsx}\"",
"typecheck": "tsc --noEmit"The format script carries an explicit glob over two file extensions. The lint script is a bare invocation with no path and no pattern at all, so whatever it covers is defined entirely inside the lint configuration file rather than in the script.
That configuration file is in the tree, and its contents are not something the page describes, so a reader cannot tell whether the two scopes match. They plausibly do, and it is a common way to write a flat-config setup. But it means the set of linted files is invisible in the one place a contributor will look, and invisible in a different way from the formatted set.
The typecheck script is the third of the trio and has neither a path nor a flag beyond disabling emit, which is the conventional form.
The rest of the tree is small and conventional for this kind of template: an application directory, a components directory, a hooks directory, the shared library directory holding the model list and the utility module, the tools directory, a components configuration file for the component tool, a Next configuration file in TypeScript, a lockfile and a workspace file. There is also a directory for assistant tool configuration and an agent context file, neither of which the page mentions.
Editorial conclusion
This is the rare template that tells you its own security posture before you deploy it, and that makes it a better starting point than most. The streaming route is public and unauthenticated by design, every request spends gateway credits, and the page names the failure mode in three words: denial of wallet. It then gives you the two controls that matter, a rate limit and a spend cap, plus the note that per-request bounds are not the same thing as volume bounds. Who should use it: anyone who wants a working streaming chat with tool calls, typed tool parts and a human-in-the-loop loop, and who will read the security section before putting it on a domain. Who should not: anyone deploying it unchanged, because the page's own advice is that it is fine for a personal demo and not fine for real traffic. Three things to check first. Read the dependency list before installing, because a two-character class-name utility is listed as a runtime package alongside a source directory that conventionally holds it, and a fresh install will try to fetch it from the registry. Decide how you will name models, because there is no environment variable for it: the default is whichever entry comes first in one source file, and the same file doubles as the request allowlist. And think about where the code runs, because the local setup path assumes a linked hosting project or a dashboard-issued key, so there is no configuration path that does not involve that platform. What this is not is a product. It is four features, three tools and one route, with no authentication, no persistence and no observability, and the page is explicit that adding the first two is your job.
Frequently asked questions
Is the chatbot-template chat route safe to deploy publicly?
Not unchanged. The page states the route is public and unauthenticated, that every request spends the deployer's gateway credits, and calls the risk denial of wallet. It recommends rate limiting, a spend cap, and adding authentication if the chatbot is not meant to be public, and notes that the existing per-request guards bound one request rather than overall volume.
What environment variables does chatbot-template need?
One, a gateway API key, and only for local development. Deployments on the hosting platform authenticate automatically through its own token exchange and bill the team's credits. The local path is either linking the checkout to a project and pulling its environment, or creating a key in the dashboard and copying it into a local environment file.
How do I change the default model in chatbot-template?
There is no environment variable for it. The model list lives in one source file and the first entry is the default, so changing the default is an array reordering. The same file is the allowlist the route restricts requests to, so adding a model also makes it reachable from the public route.
How do I add a tool to chatbot-template?
Four edits: create a file under the tools directory whose name is the model-facing tool name and export a tool definition with a description, an input schema and, for tools the server runs, an execute function; register it in the tools index; add a component under the parts directory; and add a matching case to the switch in the message renderer.
What tools ship with the chatbot-template?
Three: a server-executed repository lookup for a code host, an interactive questionnaire tool with no execute function because the user answers it in the interface, and provider-native web search. Message part types are inferred from the tool definitions, so tool input and output are fully typed in each part component.
What does the shadcn chatbot-template actually include?
Four features: streaming chat with markdown rendering, a tool calling example, web search through each provider's own built-in search tool, and a human-in-the-loop questionnaire where the model can ask clarifying questions. There is no authentication, no persistence layer and no analytics in the template itself.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/shadcn-ui-chatbot-template)Star history chart for your README
[](https://hysenlabs.com/projects/shadcn-ui-chatbot-template)Was this useful?