Configure matching public modes, render ChatLayout, and define abuse controls and storage behavior before exposing anonymous model access.

A public chatbot can answer questions without creating user accounts or storing conversation history in your application database. It still needs explicit server access, a matching client mode, and controls on the work each request can trigger.
BTST's AI Chat plugin supports that combination through public mode. This guide targets @btst/stack@3.0.2 and assumes the shared BTST API handler, client stack, providers, and styles are already installed.
The backend uses access: "public"; the client uses mode: "public". They configure different parts of the flow:
| Configuration | Responsibility |
|---|---|
Backend access: "public" | Permit the maintained stateless streaming operations |
Client mode: "public" | Render the public conversation interface without server history |
| Shared stack API runtime | Send requests to the correct handler |
ChatLayout | Render the chat interface or widget |
Changing only the client does not make the server public. Removing authorization rules from the default authorized backend is also not a public-mode configuration.
This server helper takes your existing model and a required application-owned pre-chat guard:
import type { LanguageModel } from "ai";
import {
aiChatBackendPlugin,
type AiChatBackendHooks,
} from "@btst/stack/plugins/ai-chat/api";
export function createPublicChatPlugin(
model: LanguageModel,
beforeChat: NonNullable<AiChatBackendHooks["onBeforeChat"]>,
) {
return aiChatBackendPlugin({
model,
access: "public",
systemPrompt: "Answer questions about the public product documentation.",
hooks: { onBeforeChat: beforeChat },
});
}
Register the returned plugin under aiChat in your backend stack. The helper intentionally requires a guard; it does not implement rate limiting or choose a model. Supply a model supported by your installed AI SDK/provider versions and keep provider credentials on the server. The example prompt does not load your documentation: provide verified product context through your application before presenting the bot as a documentation assistant.
In the client stack, register the matching mode:
import { aiChatClientPlugin } from "@btst/stack/plugins/ai-chat/client";
export const publicChatClient = aiChatClientPlugin({ mode: "public" });
Use that plugin under aiChat, retaining the existing stack's API, site, and QueryClient runtime. Inside its provider, a widget can render with:
"use client";
import { ChatLayout } from "@btst/stack/plugins/ai-chat/client";
export function PublicChatWidget() {
return <ChatLayout layout="widget" />;
}
ChatLayout takes its mode from the registered plugin. Do not add another component-level mode setting.
In public mode, the plugin does not persist conversation or message history. Conversation history and CRUD endpoints remain unavailable with 404 responses. This does not make the rest of your application public.
The browser still sends chat messages to the server and model provider. Stateless application storage is not a promise that no infrastructure, provider, or logging system processes or retains data. Explain the actual behavior to visitors and keep sensitive user data out of diagnostic logs.
By default, refreshing the page loses the public conversation. ChatLayout exposes initialMessages and onMessagesChange for application-managed browser storage. If you add persistence, decide what is saved, when it expires, and how the visitor clears it. Validate stored data before reuse and avoid reading browser-only storage during server rendering.
For account-linked history and ownership checks, use the private conversation guide instead.
Your onBeforeChat guard should reject requests that exceed the application's policy before they reach the provider. For a deployed service, use a rate limiter shared across instances, with a defined expiry and a trustworthy identity source. An unbounded module-level map is not a durable quota system, and an arbitrary forwarded-IP header is not automatically trusted.
Also consider request-body limits, input length, concurrent streams, provider spending limits, and cancellation. A request count alone does not bound the cost of a very large prompt or long-running tool. Keep these controls in the handler, infrastructure, guard, or provider integration that actually supports them; the released plugin config does not expose a generic maxOutputTokens option.
The example registers no tools or page-context features. Add them only for a defined use case. A public tool must independently validate its input and authority before reading private records or making changes. A system prompt is not an access-control boundary.
Test an anonymous message, an intentionally rejected request, a failed model response, and an interrupted stream. Verify that the guard prevents provider work when it denies a request. Check that history endpoints remain unavailable and that refreshing behaves as your storage policy describes.
For TanStack Start, the streaming handler guide covers response forwarding. In any framework, test a deployed stream rather than assuming that a successful local response proves production streaming works.
The AI Chat documentation, released backend config, and operation implementation are the source references for these public-mode guarantees.