BTST
PluginsQuickstartDocs
Live Blog

From research to product evaluation

Evaluating a publishing workflow for an app you already own?

See what the BTST Blog plugin adds to an existing React or Next.js app
BTST

Open-source TypeScript features for the React application, data, and deployment you already own.

Released plugins

  • Blog
  • AI Chat
  • CMS
  • Form Builder
  • UI Builder
  • Kanban
  • Comments
  • Media
  • Route Docs
  • OpenAPI
  • Better Auth UI

Resources

  • Quickstart
  • Documentation
  • All plugins
  • Live Blog
  • GitHub (opens in a new tab)
© 2026 BTST. Open source under the MIT License.
AI Chat
September 14, 2026ReactAI Chat

Add a Stateless Public AI Chat Widget to React with BTST

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

Add a Stateless Public AI Chat Widget to React with BTST

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.

Set the backend and client modes together#

The backend uses access: "public"; the client uses mode: "public". They configure different parts of the flow:

ConfigurationResponsibility
Backend access: "public"Permit the maintained stateless streaming operations
Client mode: "public"Render the public conversation interface without server history
Shared stack API runtimeSend requests to the correct handler
ChatLayoutRender 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:

TS
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
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:

TS
  1. 1
  2. 2
  3. 3
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:

TSX
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
"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.

Define what stateless means#

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.

Bound public work before provider execution#

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.

Verify behavior through the real handler#

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.

In This Post

Set the backend and client modes togetherDefine what stateless meansBound public work before provider executionVerify behavior through the real handler