Chatbot Message Persistence
Being able to store and load chat messages is crucial for most AI chatbots. In this guide, we’ll show how to implement message persistence withuseChat and streamText.
This guide does not cover authorization, error handling, or other real-world
considerations. It is intended to be a simple example of how to implement
message persistence.
Starting a new chat
When the user navigates to the chat page without providing a chat ID, we need to create a new chat and redirect to the chat page with the new chat ID.Loading an existing chat
When the user navigates to the chat page with a chat ID, we need to load the chat messages from storage. TheloadChat function in our file-based chat store is implemented as follows:
Validating messages on the server
When processing messages on the server that contain tool calls, custom metadata, or data parts, you should validate them usingvalidateUIMessages before sending them to the model.
Validation with tools
When your messages include tool calls, validate them against your tool definitions:Handling validation errors
Handle validation errors gracefully when messages from the database don’t match current schemas:Displaying the chat
Once messages are loaded from storage, you can display them in your chat UI. Here’s how to set up the page component and the chat display:useChat hook to manage the conversation:
Storing messages
useChat sends the chat id and the messages to the backend.
The
useChat message format is different from the ModelMessage format. The
useChat message format is designed for frontend display, and contains
additional fields such as id and createdAt. We recommend storing the
messages in the useChat message format.When loading messages from storage that contain tools, metadata, or custom data
parts, validate them using validateUIMessages before processing (see the
validation section above).onFinish callback of the toUIMessageStreamResponse function.
onFinish receives the complete messages including the new AI response as UIMessage[].
saveChat function, which in
our file-based chat store is implemented as follows:
Message IDs
In addition to a chat ID, each message has an ID. You can use this message ID to e.g. manipulate individual messages.Client-side vs Server-side ID Generation
By default, message IDs are generated client-side:- User message IDs are generated by the
useChathook on the client - AI response message IDs are generated by
streamTexton the server
Setting Up Server-side ID Generation
When implementing persistence, you have two options for generating server-side IDs:- Using
generateMessageIdintoUIMessageStreamResponse - Setting IDs in your start message part with
createUIMessageStream
Option 1: Using generateMessageId in toUIMessageStreamResponse
You can control the ID format by providing ID generators using createIdGenerator():
Sending only the last message
Once you have implemented message persistence, you might want to send only the last message to the server. This reduces the amount of data sent to the server on each request and can improve performance. To achieve this, you can provide aprepareSendMessagesRequest function to the transport.
This function receives the messages and the chat ID, and returns the request body to be sent to the server.
Handling client disconnects
By default, the AI SDKstreamText function uses backpressure to the language model provider to prevent
the consumption of tokens that are not yet requested.
However, this means that when the client disconnects, e.g. by closing the browser tab or because of a network issue,
the stream from the LLM will be aborted and the conversation may end up in a broken state.
Assuming that you have a storage solution in place, you can use the consumeStream method to consume the stream on the backend,
and then save the result as usual.
consumeStream effectively removes the backpressure,
meaning that the result is stored even when the client has already disconnected.
In production applications, you would also track the state of the request (in
progress, complete) in your stored messages and use it on the client to cover
the case where the client reloads the page after a disconnection, but the
streaming is not yet complete.