NE

nextjs-chatbot

Provides an opinionated, production-ready framework for building web chatbots with tool calling and database session storage.

Install

mkdir -p .claude/skills/nextjs-chatbot && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/16946" && unzip -o skill.zip -d .claude/skills/nextjs-chatbot && rm skill.zip

Installs to .claude/skills/nextjs-chatbot

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Advanced patterns for production Next.js web chatbots built with AI SDK 6 + ai-elements. Covers tool calling with human-in-the-loop (HITL) approval, PostgreSQL session persistence, GDPR consent gating, SQL-first search, per-tool UI rendering, popup widget embedding, message feedback, follow-up suggestions, scope enforcement, and evals. Use when building a customer support bot, conversational interface, or any web chatbot needing tool approval, database sessions, or custom tool output components. Not a scaffolding tool — use `/ai-app` to scaffold from scratch, `/ai-sdk-6` for general SDK questions, `/ai-elements` for chat UI components, `/vercel:chat-sdk` for multi-platform (Slack/Teams/Discord) bots.
709 chars✓ has a “when” triggerlonger than Claude Code's old 250-char listing cap (fine on current versions)
Advanced

Key capabilities

  • Implement human-in-the-loop (HITL) approval for tool calls
  • Manage PostgreSQL session persistence for chatbot conversations
  • Enforce GDPR consent gating for chatbot interactions
  • Perform SQL-first search within the chatbot context
  • Render custom UI components per tool state

How it works

The skill provides advanced patterns for production Next.js web chatbots, focusing on features like HITL approval, PostgreSQL session persistence, GDPR consent, and custom tool UI rendering.

Inputs & outputs

You give it
user messages, chat ID, and consent status
You get back
streaming text responses from the assistant, rendered tool UIs, and persistent chat sessions

When to use nextjs-chatbot

  • Build customer support bots
  • Implement tool approval flows
  • Set up PostgreSQL session persistence
  • Create conversational interfaces

About this skill

Next.js Chatbot

Opinionated blueprint for production web chatbots. Focuses on patterns not covered by /ai-sdk-7, /ai-elements, or /nextjs-shadcn — use those skills for general SDK, component, and framework questions. For multi-platform bots (Slack, Teams, Discord), use /vercel:chat-sdk instead.

Stack defaults

  • Runtime: bun
  • Model: a non-reasoning flagship with reasoning effort off — chat latency is the product. Read the version from the provider's live catalog, not from here.
  • AI SDK: ai@7ToolLoopAgent, createAgentUIStreamResponse. ⚠️ Check package.json — on a v6 codebase the names below are wrong; see If the project is still on ai@6.
  • UI: shadcn/ui (Base UI base) + ai-elements (see /ai-elements for component docs)
  • Scroll: @shadcn/react MessageScroller — don't hand-roll stick-to-bottom
  • Markdown: shadcn typeset (typeset typeset-chat), streaming-stable
  • ORM: Drizzle + PostgreSQL
  • State: Zustand for client-side chat state (consent, session, suggestions)
  • Attachments: See /ai-elements Attachments component for file upload

Recommended MCP servers

  • next-devtools (next-devtools-mcp@latest via npx) — route inspection, build diagnostics. See nextjs.org/docs/app/guides/mcp
  • ai-elements (via mcp-remotehttps://registry.ai-sdk.dev/api/mcp) — component registry search

Add both to the project's .mcp.json (claude mcp add writes it for you); .claude/settings.json only enables and permits servers, it does not define them.

Agent setup

export function createAgent(opts?: { model?: LanguageModel }) {
  return new ToolLoopAgent({
    model: opts?.model ?? openai(CHAT_MODEL), // one constant, read from env
    instructions,                             // NOT `system` — that is v6
    reasoning: "low",                         // portable top-level, see below
    tools,
    stopWhen: isStepCount(10),
  });
}
export const agent = createAgent();
export type AgentUIMessage = InferAgentUIMessage<typeof agent>;

Export both factory and singleton — factory needed for benchmarks. Wrap with devToolsMiddleware() in dev.

⚠️ Reasoning effort is the portable top-level reasoning, not providerOptions.openai.reasoningEffort — set both and the top-level one is silently ignored, so your setting does nothing. Raising it above "low" means keeping sendReasoning: false on the stream: the Responses API rejects a reasoning part that comes back on the next turn without its required follower, and a context trimmer that strips step-start/tool-* usually doesn't strip this one.

Route handler

export const maxDuration = 60;

export async function POST(request: Request) {
  const { messages, chatId, ...consent } = await request.json();
  // 1. Validate consent — return 403 if missing
  // 2. Await session upsert BEFORE streaming (FK dependency)
  return createAgentUIStreamResponse({
    agent,
    uiMessages: messages,
    generateMessageId: createIdGenerator({ prefix: "msg", size: 16 }),
    consumeSseStream: ({ stream }) => consumeStream({ stream }),
    experimental_transform: smoothStream({ delayInMs: 15, chunking: "word" }),
    sendReasoning: false,
    onEnd: async ({ responseMessage }) => { /* save — see persistence.md */ },
  });
}

Azure OpenAI model routing

Azure's Responses API does support non-reasoning models (gpt-4o), but multi-turn tool calls hit an intermittent 400 Item with id 'fc_...' not found error (Microsoft Q&A). Workaround: route non-reasoning models to Chat Completions (azure.chat()); reasoning models (gpt-5.x, o-series) use Responses API (default):

const isReasoning = /^(o[1-9]|gpt-5)/.test(deployment);
export const chatModel = isReasoning ? azure(deployment) : azure.chat(deployment);

Set reasoning only for reasoning models to avoid warnings.

If the project is still on ai@6

Everything on this page is patterns, not API surface, so it carries over — but the snippets above are v7 names and v6 doesn't know them. Some fail loudly (an undefined import); the dangerous ones fail quietly — a callback that never fires or a step limit that never applies, which reads as a model bug rather than a typo. Check package.json and translate back:

v7v6
instructionssystem
isStepCountstepCountIs
onEnd (on the stream)onFinish — the client useChat onFinish is a different, live callback and keeps its name in both
telemetryexperimental_telemetry
reasoning: "low"providerOptions: { openai: { reasoningEffort: "low" } }
agent-level toolApproval: { myTool: "user-approval" }needsApproval: true on the tool() — one of the quiet ones: left on a v7 tool it is simply ignored and the tool executes without asking

Use /ai-sdk-6 for the rest; /ai-sdk if you don't know the version yet.

Never let raw error text reach the browser

onError streams its return value verbatim to the client. String(error) there puts a provider 401, an endpoint URL or a Postgres constraint on a customer's screen — and logs nothing, so you find out from a screenshot.

onError: (error) => {          // log the real thing, return a sentence
  console.error("[chat]", error);
  return "Something went wrong generating the answer. Try again.";
}

The client needs the same guard separately: a non-2xx response never goes through the stream, so useChat's onError receives an Error whose message is the raw response body. Map it to a fixed set of sentences before rendering.

Custom streams need X-Accel-Buffering: no

Only for responses you build yourself — NDJSON upload progress, an SSE grid fill. createAgentUIStreamResponse already sets it. Without the header a buffering proxy (nginx, Cloud Run) holds the whole stream and delivers the progress bar in one jump at the end, which is worse than having no progress bar. Pair it with Cache-Control: no-store, no-transform.

⚠️ An error emitted inside an already-open stream arrives with HTTP 200 — the status is long gone. Rejections that happen before the stream opens do use real codes. Client code has to handle both shapes.

Client transport patterns

Dynamic context via transport body

Inject per-request context (e.g., a saved document for edit mode) from the client:

// Simple: body function on DefaultChatTransport
const transport = new DefaultChatTransport({
  api: "/api/chat",
  body: () => ({ documentContext: activeDocRef.current }),
});

// Fine-grained: prepareSendMessagesRequest (official API)
const transport = new DefaultChatTransport({
  prepareSendMessagesRequest: ({ id, messages }) => ({
    body: { id, message: messages.at(-1), context: extraRef.current },
  }),
});

Server reads extra fields from the request body and passes to agent factory.

Chat remount (new conversation)

Always call stop() before clearing — otherwise the active stream writes into the new conversation:

const { messages, sendMessage, stop, setMessages } = useChat({ transport });

const startNew = useCallback(() => {
  stop();                     // Cancel active stream FIRST
  setMessages([]);
  clearStoredMessages();      // If using localStorage
  setChatId(crypto.randomUUID());
  setConversationKey(k => k + 1);
}, [stop, setMessages]);

localStorage persistence (no DB)

For lightweight chatbots that don't need server-side persistence:

// Load on init via messages prop (NOT useEffect + setMessages)
const initialMessages = useMemo(() => {
  const stored = loadStoredMessages();
  return stored?.length ? (stored as UIMessage[]) : undefined;
}, []);

const { messages, sendMessage } = useChat({
  transport,
  messages: initialMessages,    // useChat accepts initial messages
  onFinish: ({ messages: all }) => saveStoredMessages(all),
});

Hydration: Zustand + localStorage

Zustand stores that read localStorage in create() cause React hydration mismatch (server: false, client: true). Fix with a mounted gate:

const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);

// In render:
{!mounted || !hasConsented ? <ConsentGate /> : <Chat />}

Same class of bug, different cause: relative timestamps ("6 seconds ago") on a server-rendered history list. The server and the client compute a different string, React calls it a mismatch and throws the whole subtree away to re-render it. Either render an absolute, locale-independent date, or gate the relative one behind the same mounted flag.

Adding a new tool

  1. Create lib/ai/tools/my-tool.ts with tool() from ai
  2. Export from lib/ai/tools/index.ts
  3. Add to tools object in the agent file
  4. Document in the agent's instructions string
  5. Add UI renderer in chat-message.tsx (handle tool-myTool part type)

Structured output tools (schema-as-output)

When the tool generates structured data (not query/compute), use the pass-through pattern — the Zod schema defines the output, execute just validates and returns:

const generateDocTool = tool({
  description: "Generate structured documentation",
  inputSchema: MyDocSchema,           // Zod schema IS the output shape
  execute: async (data) => data,       // Validate and return
});

LLM-resilient enums — LLMs sometimes append extra text to enum values. Use lenient transforms:

const LenientCategory = z.string().transform((val) => {
  const valid = ["Business", "Technical", "Legal"] as const;
  return valid.find((c) => val.startsWith(c)) ?? "Business";
});

Building a new chatbot

When scaffolding from scratch, read checklist.md for the full setup sequence.

Theming

Theme setup belongs to /nextjs-shadcn: oklch variables in globals.css,


Content truncated.

When not to use it

  • When scaffolding a chatbot from scratch (use `/ai-app` instead)
  • When general AI SDK questions arise (use `/ai-sdk-6` instead)
  • When only chat UI components are needed (use `/ai-elements` instead)

Limitations

  • Not a scaffolding tool
  • Focuses on web chatbots, not multi-platform bots
  • Requires specific AI SDK 6 and ai-elements patterns

How it compares

This skill offers opinionated blueprints for production web chatbots with specific patterns for HITL approval, database sessions, and custom tool output components, unlike general SDKs or UI libraries.

Compared to similar skills

nextjs-chatbot side by side with the closest alternatives in the catalog.

SkillInstallsUpdatedSafetyDifficulty
nextjs-chatbot (this skill)02moNo flagsAdvanced
add-feature05moNo flagsIntermediate
nextjs-supabase-auth126moNo flagsIntermediate
app-specific-patterns42moNo flagsIntermediate

Try saying

Example prompts that trigger this skill in your AI assistant.

You might also like

add-feature

jujunjun110

新しい機能を追加する。ユーザーが機能の説明をしたとき、DDD4層に従って全レイヤーのファイルを生成する

00

nextjs-supabase-auth

davila7

Expert integration of Supabase Auth with Next.js App Router Use when: supabase auth next, authentication next.js, login supabase, auth middleware, protected route.

1259

app-specific-patterns

growilabs

GROWI main application (apps/app) specific patterns for Next.js, Jotai, SWR, and testing. Auto-invoked when working in apps/app.

46

javascript-typescript-typescript-scaffold

sickn33

You are a TypeScript project architecture expert specializing in scaffolding production-ready Node.js and frontend applications. Generate complete project structures with modern tooling (pnpm, Vite, N

31

server-actions

davepoon

This skill should be used when the user asks about "Server Actions", "form handling in Next.js", "mutations", "useFormState", "useFormStatus", "revalidatePath", "revalidateTag", or needs guidance on data mutations and form submissions in Next.js App Router.

11

web-frameworks

mrgoonie

Build modern full-stack web applications with Next.js (App Router, Server Components, RSC, PPR, SSR, SSG, ISR), Turborepo (monorepo management, task pipelines, remote caching, parallel execution), and RemixIcon (3100+ SVG icons in outlined/filled styles). Use when creating React applications, implementing server-side rendering, setting up monorepos with multiple packages, optimizing build performance and caching strategies, adding icon libraries, managing shared dependencies, or working with TypeScript full-stack projects.

10

Search skills

Search the agent skills registry