This example demonstrates how to persist chat messages using Vercel AI SDK with a Postgres database in a Next.js application. It uses:
- AI SDK for streaming chat responses
- Postgres for message storage
- Drizzle ORM for database queries
- Next.js for the web application
The chat messages are stored in a Postgres database using the schema defined in schema.ts
. Each chat has an auto-incrementing ID and contains multiple messages with timestamps.
- Create postgres database
createdb ai-sdk-demo
# or use PgAdmin or other postgres tools
- Add environment variables to .env:
DATABASE_URL=postgres://username:password@localhost:5432/ai-sdk-demo
OPENAI_API_KEY=your_openai_api_key_here
- Install dependencies:
pnpm install
- Push database schema:
pnpm db:push
- Start development server:
pnpm run dev
This application uses a prefix-based approach for persisting messages with PostgreSQL. Here's how it works:
The database uses three main tables with a prefix-based column naming convention:
The schema can be found in lib/db/schema.ts
.
chats
: Stores chats with auto-generated IDsmessages
: Individual messages with role, timestamp, and chat referenceparts
: Message content parts using prefix-based columns
Each message part type has dedicated columns with specific prefixes:
text_*
: Text content partsreasoning_*
: Reasoning/thinking partsfile_*
: File attachmentssource_url_*
: URL sourcessource_document_*
: Document sourcestool_[toolName]_*
: Tool calls (e.g.,tool_getWeatherInformation_*
)data_[dataType]_*
: Custom data parts (e.g.,data_weather_*
)
This approach avoids complex polymorphic relationships while maintaining type safety and query performance.
The schema enforces data integrity through carefully designed constraints:
- Complete Part Definitions: Each message part type requires all relevant columns to be populated together. For example, source URL parts must have both
source_url_url
andsource_url_description
fields defined. - Tool Call Consistency: Tool-related columns for the same tool must be provided as a complete set (input, output, state, etc.).
These constraints prevent partial or corrupted message parts from being stored, ensuring reliable message reconstruction during retrieval.
The system provides bidirectional conversion between UI messages and database storage (lib/utils/message-mapping.ts
):
- Message parts are flattened into database rows
- Each part gets an order index to maintain sequence
- Tool states and provider metadata are preserved
- Database rows are reconstructed into typed message parts
- Tool states and custom data are properly restored
- Type safety is maintained throughout
Server-side functions (lib/db/actions.ts
) handle all database operations:
createChat()
: Creates a new chat sessionupsertMessage()
: Inserts/updates messages with atomic transactionsloadChat(chatId)
: Retrieves all messages for a chatgetChats()
: Lists all chat sessionsdeleteChat(chatId)
: Removes a chat and all associated datadeleteMessage(messageId)
: Deletes a message and all subsequent messages