# AI SDK (with Next.js) (/docs/guides/integrations/ai-sdk) > For the complete Prisma documentation index, see [llms.txt](https://www.prisma.io/docs/llms.txt). A markdown version of any docs page is available by appending `.md` to its URL. Build a chat application with AI SDK, Prisma ORM, and Next.js that stores chat sessions and messages in Prisma Postgres. Location: Guides > Integrations > AI SDK (with Next.js) ## Introduction [#introduction] [AI SDK](https://ai-sdk.dev/) streams model responses to the browser, and a [Next.js](https://nextjs.org/) route handler gives you a place to persist each exchange with Prisma ORM, so a conversation survives page reloads. In this guide, you scaffold a Next.js app with Prisma ORM, define a contract for chat sessions and messages, save every completed exchange from the AI SDK route handler, and load a session back into the UI when the page opens. > [!NOTE] > Using Prisma ORM 7? > > Prisma ORM 8 is a [release candidate](https://www.prisma.io/docs/orm/release-status). Prisma ORM 7 remains fully supported; the Prisma ORM 7 version of this guide is at [/guides/v7/integrations/ai-sdk](https://www.prisma.io/docs/guides/v7/integrations/ai-sdk). ## Prerequisites [#prerequisites] * [Node.js](https://nodejs.org) 22.18 or newer on the 22 line, 24.11 or newer on the 24 line, or 26 or newer (24 recommended) * An [OpenAI API key](https://platform.openai.com/api-keys) * A PostgreSQL connection string, or nothing at all: `npx create-db@latest` can create a [Prisma Postgres](https://www.prisma.io/docs/postgres) database for you ## Use with your agent [#use-with-your-agent] To delegate this guide to your coding agent, copy the prompt below and hand it over: ```text Build a Next.js chat app with AI SDK that stores chat sessions and messages with Prisma ORM, following https://www.prisma.io/docs/guides/integrations/ai-sdk.md. 1. Scaffold: `npx create-prisma@latest create ai-sdk-prisma --template next --provider postgres --yes`. Then run `npx prisma@latest init` in `ai-sdk-prisma` so the Prisma agent skills are installed and stay current, and use them. Get a database connection string: use the one I give you, or create a Prisma Postgres database with `npx create-db@latest` and show me the claim URL it prints. Export it as `DATABASE_URL` in the shell; the Prisma CLI reads the environment variable, not `.env`. 2. Replace `src/prisma/contract.prisma` with the `Session` and `Message` models from the guide (a `MessageRole` enum, `parts` stored as `Json`, `position` for ordering, `onDelete: Cascade` on the relation). Delete the starter files the template generated for its own schema: `src/prisma/users.ts`, `src/prisma/seed.ts`, and the `migrations/` directory. Run `npm run contract:emit` and then `npm run db:init` with `DATABASE_URL` exported. 3. Install `ai`, `@ai-sdk/react`, and `@ai-sdk/openai`. Put `OPENAI_API_KEY` and `DATABASE_URL` in `.env`. 4. Create `src/prisma/chat.ts` with `saveChat(id, messages)` (upsert the session, then upsert each message by id inside `db.transaction`) and `loadChat(id)` (messages for a session ordered by `position`), `src/app/api/chat/route.ts` (streamText with `openai("gpt-5.1")`, `toUIMessageStream` with `originalMessages`, `generateMessageId`, and an `onEnd` that calls `saveChat`), `src/app/api/messages/route.ts` (GET, `?chat=`), a server `src/app/page.tsx` that redirects to `/?chat=` when the id is missing, and a client `src/app/chat.tsx` built on `useChat({ id })` that fetches `/api/messages` on mount. 5. Run `npx tsc --noEmit` and `npm run build`. Start `npm run dev` in the background, open http://localhost:3000, send a message, reload the page, and confirm the conversation is still there. Stop the dev server when done. ``` ## 1. Scaffold the project [#1-scaffold-the-project] The `next` template generates a Next.js app with Prisma ORM already wired in: the contract, the client in `src/prisma/db.ts`, and package scripts for the database steps. That replaces the `create-next-app`, `prisma init`, driver adapter, and `prisma generate` steps of the Prisma ORM 7 guide. #### bun ```bash bunx create-prisma@latest create ai-sdk-prisma --template next --provider postgres --no-deploy cd ai-sdk-prisma ``` #### pnpm ```bash pnpm dlx create-prisma@latest create ai-sdk-prisma --template next --provider postgres --no-deploy cd ai-sdk-prisma ``` #### yarn ```bash yarn dlx create-prisma@latest create ai-sdk-prisma --template next --provider postgres --no-deploy cd ai-sdk-prisma ``` #### npm ```bash npx create-prisma@latest create ai-sdk-prisma --template next --provider postgres --no-deploy cd ai-sdk-prisma ``` Answer the prompts for contract authoring style (this guide uses PSL), package manager, and agent skills. The scaffold installs dependencies and emits the contract your queries are type-checked against. Next, set the database connection for the CLI steps. Use your own PostgreSQL connection string, or create a Prisma Postgres database with `npx create-db@latest`; it prints a connection string and a claim URL; claim it with a free Prisma account to keep the database. Export the variable in the shell you work in; the Prisma CLI reads the environment variable, not `.env`: ```bash export DATABASE_URL="" ``` ## 2. Define the data contract [#2-define-the-data-contract] The template ships a `User` and `Post` starter schema. Replace the whole of `src/prisma/contract.prisma` with the chat models: ```prisma title="src/prisma/contract.prisma" // use prisma-8 enum MessageRole { @@type("pg/text@1") user = "user" assistant = "assistant" } model Session { id String @id createdAt TimestamptzString @default(now()) updatedAt DateTime(as: string) @onCreate(now()) @onUpdate(now()) messages Message[] } model Message { id String @id role MessageRole parts Json position Int createdAt TimestamptzString @default(now()) sessionId String session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade) } ``` A few things to note: * `Session.id` and `Message.id` have no default. AI SDK generates a chat id on the client and stable message ids on the server, and you store those. * `MessageRole` is a Prisma ORM enum: it is stored as `text` and enforced with a `CHECK` constraint. The values match AI SDK's `role` strings, so you can save `message.role` as is. * `parts` is `Json`. AI SDK messages carry an array of parts (text, reasoning, tool calls), and storing it verbatim means the UI can render a saved message exactly like a live one. * `position` records where a message sits in the conversation, so `loadChat` can return messages in order. The template also generated a seed helper and an initial migration for the starter schema. Remove them so nothing references models that no longer exist: ```bash rm src/prisma/users.ts src/prisma/seed.ts rm -r migrations ``` Emit the contract and initialize the database: #### bun ```bash bun run contract:emit bun run db:init ``` #### pnpm ```bash pnpm run contract:emit pnpm run db:init ``` #### yarn ```bash yarn contract:emit yarn db:init ``` #### npm ```bash npm run contract:emit npm run db:init ``` ```text no-copy "summary": "Applied 4 operation(s) across 1 space(s), database signed" ``` `contract:emit` regenerates `src/prisma/contract.json` and `src/prisma/contract.d.ts`, which is where your query types come from. `db:init` creates the `session` and `message` tables, the index on `sessionId`, and the cascading foreign key, then signs the database. There is no separate `generate` step and no client to instantiate: `src/prisma/db.ts` already exports `db`, and its types follow the contract you just emitted. If `db:init` stops with `Connection terminated unexpectedly`, a database you just created is still starting; wait a few seconds and run it again. ## 3. Install AI SDK and set your API key [#3-install-ai-sdk-and-set-your-api-key] #### bun ```bash bun add ai @ai-sdk/react @ai-sdk/openai ``` #### pnpm ```bash pnpm add ai @ai-sdk/react @ai-sdk/openai ``` #### yarn ```bash yarn add ai @ai-sdk/react @ai-sdk/openai ``` #### npm ```bash npm install ai @ai-sdk/react @ai-sdk/openai ``` Add your OpenAI API key to `.env`. Put `DATABASE_URL` there too: `next dev` and `next build` load `.env`, so the app finds both values without the shell export. The Prisma CLI does not read `.env`, which is why you still export `DATABASE_URL` for `db:init` and the other `prisma` commands. ```bash title=".env" DATABASE_URL="" OPENAI_API_KEY="" ``` ## 4. Save and load chats with Prisma ORM [#4-save-and-load-chats-with-prisma-orm] Create `src/prisma/chat.ts` with two functions: `saveChat` persists a session and its messages, and `loadChat` reads them back in order. ```typescript title="src/prisma/chat.ts" import type { JsonValue } from "@prisma/orm-postgres/target/codec-types"; import type { UIMessage } from "ai"; import { db } from "./db.ts"; export async function saveChat(id: string, messages: UIMessage[]) { await db.transaction(async (tx) => { await tx.orm.public.Session.upsert({ create: { id }, update: {}, conflictOn: { id }, }); for (const [position, message] of messages.entries()) { if (message.role !== "user" && message.role !== "assistant") continue; await tx.orm.public.Message.upsert({ create: { id: message.id, sessionId: id, role: message.role, parts: message.parts as JsonValue, position, }, update: { parts: message.parts as JsonValue, position }, conflictOn: { id: message.id }, }); } }); } export async function loadChat(id: string): Promise { const rows = await db.orm.public.Message.select("id", "role", "parts") .where({ sessionId: id }) .orderBy((m) => m.position.asc()) .all(); return rows.map((row) => ({ id: row.id, role: row.role, parts: row.parts as UIMessage["parts"], })); } ``` How it works: * Model access is namespace-qualified on PostgreSQL: `db.orm.public.Session`, not `db.session`. `db.transaction` hands you a `tx` with the same `orm` surface, and everything inside it commits together or not at all. * `upsert` takes `create`, `update`, and `conflictOn`. AI SDK sends the whole conversation with every request, so upserting each message by its id keeps saves idempotent: messages that already exist are left alone, and the new user and assistant messages are inserted. * `message.parts` is typed by AI SDK as an array of part objects. The `as JsonValue` cast tells Prisma ORM to store it in the `Json` column; the reverse cast in `loadChat` hands it back to AI SDK as `UIMessage["parts"]`. * `.select("id", "role", "parts")` narrows the returned row type to the fields the UI needs, and `.orderBy((m) => m.position.asc())` restores conversation order. ## 5. Create the chat route handler [#5-create-the-chat-route-handler] The route handler streams the model response to the browser and, once the stream ends, saves the full conversation: ```bash mkdir -p src/app/api/chat ``` ```typescript title="src/app/api/chat/route.ts" import { openai } from "@ai-sdk/openai"; import { convertToModelMessages, createIdGenerator, createUIMessageStreamResponse, streamText, toUIMessageStream, type UIMessage, } from "ai"; import { saveChat } from "../../../prisma/chat"; export const maxDuration = 300; export async function POST(req: Request) { const { messages, id }: { messages: UIMessage[]; id: string } = await req.json(); const result = streamText({ model: openai("gpt-5.1"), messages: await convertToModelMessages(messages), }); return createUIMessageStreamResponse({ stream: toUIMessageStream({ stream: result.stream, originalMessages: messages, generateMessageId: createIdGenerator({ prefix: "msg", size: 16 }), onEnd: async ({ messages }) => { await saveChat(id, messages); }, }), }); } ``` This handler: 1. Reads the conversation and the chat `id` that `useChat` sends in the request body. 2. Converts the UI messages to model messages and streams the response. 3. Passes `originalMessages` so `onEnd` receives the whole conversation, including the new assistant message, and saves it with `saveChat`. `generateMessageId` gives the assistant message a stable id on the server before it is stored. Without it the id is empty, and every assistant message in a session would collide on the primary key. ## 6. Create the messages API route [#6-create-the-messages-api-route] The UI calls this route on load to restore a session: ```bash mkdir -p src/app/api/messages ``` ```typescript title="src/app/api/messages/route.ts" import { NextResponse } from "next/server"; import { loadChat } from "../../../prisma/chat"; export async function GET(req: Request) { const id = new URL(req.url).searchParams.get("chat"); if (!id) { return NextResponse.json({ error: "Missing chat id" }, { status: 400 }); } const messages = await loadChat(id); return NextResponse.json({ messages }); } ``` ## 7. Build the UI [#7-build-the-ui] The page is a server component. It reads the chat id from the URL, creates one when it is missing, and renders the chat component. Replace `src/app/page.tsx`: ```tsx title="src/app/page.tsx" import { generateId } from "ai"; import { redirect } from "next/navigation"; import Chat from "./chat"; export default async function Page({ searchParams, }: { searchParams: Promise<{ chat?: string }>; }) { const { chat } = await searchParams; if (!chat) redirect(`/?chat=${generateId()}`); return ; } ``` Keeping the id in the URL is what makes a session survive a reload: the same URL loads the same session. The chat component is a client component. `useChat({ id })` sends that id with every request, and the `useEffect` loads any saved messages into the hook when the component mounts. Create `src/app/chat.tsx`: ```tsx title="src/app/chat.tsx" "use client"; import { useChat } from "@ai-sdk/react"; import type { UIMessage } from "ai"; import { useEffect, useState } from "react"; export default function Chat({ id }: { id: string }) { const [input, setInput] = useState(""); const [loading, setLoading] = useState(true); const { messages, sendMessage, setMessages } = useChat({ id }); useEffect(() => { fetch(`/api/messages?chat=${id}`) .then((res) => res.json()) .then((data: { messages: UIMessage[] }) => { if (data.messages.length > 0) setMessages(data.messages); setLoading(false); }) .catch(() => setLoading(false)); }, [id, setMessages]); if (loading) return

Loading...

; return (
{messages.map((message) => (

{message.role === "user" ? "You" : "AI"}

{message.parts.map((part, i) => part.type === "text" ?
{part.text}
: null, )}
))}
{ e.preventDefault(); if (!input.trim()) return; sendMessage({ text: input }); setInput(""); }} > setInput(e.currentTarget.value)} />
); } ``` The template does not include Tailwind, so add a few rules to the end of `src/app/globals.css` for the bubbles and the input: ```css title="src/app/globals.css" .chat { display: flex; flex-direction: column; gap: 0.75rem; padding-bottom: 6rem; } .bubble { max-width: 80%; border-radius: 0.5rem; padding: 0.75rem 1rem; white-space: pre-wrap; background: #f0f0f0; align-self: flex-start; } .bubble.user { background: #111; color: #fff; align-self: flex-end; } .bubble .eyebrow { color: inherit; opacity: 0.6; } .composer { position: fixed; bottom: 2rem; left: 50%; width: min(40rem, calc(100% - 3rem)); transform: translateX(-50%); padding: 0.75rem; border: 1px solid #ccc; border-radius: 0.5rem; font: inherit; } ``` ## 8. Run and verify [#8-run-and-verify] Check the types first, then start the dev server: #### bun ```bash bunx tsc --noEmit bun run dev ``` #### pnpm ```bash pnpm tsc --noEmit pnpm run dev ``` #### yarn ```bash yarn tsc --noEmit yarn dev ``` #### npm ```bash npx tsc --noEmit npm run dev ``` ```text no-copy ▲ Next.js 16.1.6 (Turbopack) - Local: http://localhost:3000 - Environments: .env ✓ Ready in 5.5s ``` Open [http://localhost:3000](http://localhost:3000). The page redirects to `/?chat=`, and the first request compiles the app, so it takes a few seconds. Send a message, wait for the reply, then reload the page: both messages come back from the database. You can also read a session directly from the API. Replace the id with the one in your address bar. The session below was captured with AI SDK's mock language model instead of a live OpenAI call, which is why the assistant text reads the way it does; the persistence path is the same: ```bash curl "http://localhost:3000/api/messages?chat=" ``` ```json no-copy {"messages":[{"id":"user-1789055570091","role":"user","parts":[{"type":"text","text":"Hello from the mock test"}]},{"id":"msg-abc123def456","role":"assistant","parts":[{"type":"step-start"},{"type":"text","text":"Hi! I am a mock model.","state":"done"}]}]} ``` A session that has no messages yet returns `{"messages":[]}`, and a request without `?chat=` returns a `400`. Finally, make sure the production build passes: #### bun ```bash bun run build ``` #### pnpm ```bash pnpm run build ``` #### yarn ```bash yarn build ``` #### npm ```bash npm run build ``` ```text no-copy ✓ Compiled successfully in 30.9s Route (app) ┌ ƒ / ├ ○ /_not-found ├ ƒ /api/chat └ ƒ /api/messages ``` ## Where things live [#where-things-live] * `src/prisma/contract.prisma`: your schema. Edit it, then run `npm run contract:emit` and `npm run db:update`. * `src/prisma/db.ts`: the Prisma ORM client the template generated. It is a module-level singleton, so its connection pool is shared across requests. * `src/prisma/chat.ts`: the two functions that touch the database. * `src/app/api/chat/route.ts` and `src/app/api/messages/route.ts`: the write path and the read path. ## Common gotchas [#common-gotchas] > [!WARNING] > If the model reply is `An error occurred.` and the server log shows `AI_LoadAPIKeyError: OpenAI API key is missing`, `OPENAI_API_KEY` is not in `.env`. The route handler returns a `200` with the error inside the stream, so check the terminal running `next dev` rather than the network tab. > [!WARNING] > Do not call `db.close()` in a route handler. The client in `src/prisma/db.ts` lives for the whole process and its pool is shared across requests; close it only on [process shutdown](https://www.prisma.io/docs/orm/reference/transactions-and-runtime#close). * `db:init` and the other `prisma` scripts read `DATABASE_URL` from the environment, while Next.js reads `.env`. If a Prisma command reports a missing connection string, export the variable in that shell. * If you keep the template's `migrations/` directory and later run `npm run migration:plan`, the plan starts from the starter `User` and `Post` schema instead of from an empty database. Delete the directory before your first emit, as in step 2, and run `npx prisma migration plan --name init` when you want a checked-in migration for your own contract. * `parts` needs the `as JsonValue` cast on the way in. AI SDK types the parts array with interfaces, and Prisma ORM's `Json` input type wants plain JSON values; the cast is safe because the parts are serializable objects. ## Prompt your coding agent [#prompt-your-coding-agent] Run [`npx prisma@latest init`](https://www.prisma.io/docs/cli/init) once to install the [Prisma ORM skills](https://www.prisma.io/docs/ai/tools/skills#available-skills-for-prisma-8) for your coding agent and keep them matching your installed packages. Prompts that map to this guide: * "Using the prisma-8 skill, add a `title` field to `Session` and a `PATCH /api/sessions/:id` route that updates it." * "Add a `GET /api/sessions` route that lists sessions with their message count, using `include` with a `count()` reducer." * "Add a `DELETE /api/sessions/:id` route and confirm the cascade removes the session's messages." ## Next steps [#next-steps] * [Deploy the app to Prisma Compute](https://www.prisma.io/docs/guides/frameworks/nextjs#4-deploy-to-prisma-compute): the template already declares it in `module.ts` and `service.ts`. * [Learn the fundamentals](https://www.prisma.io/docs/orm/fundamentals/reading-data): filtering, sorting, pagination, and writes. * [Read the Prisma ORM overview](https://www.prisma.io/docs/orm) for the concepts behind contracts and typed queries. * [AI SDK documentation](https://ai-sdk.dev/docs/ai-sdk-ui/chatbot-message-persistence) on message persistence, including sending only the last message and handling client disconnects. ## Related pages - [`Datadog`](https://www.prisma.io/docs/guides/integrations/datadog): Learn how to configure Datadog tracing for a Prisma ORM project. Capture spans for every query using the @prisma/instrumentation package, dd-trace, and view them in Datadog - [`Embedded Prisma Studio (with Next.js)`](https://www.prisma.io/docs/guides/integrations/embed-studio): Learn how to embed Prisma Studio directly in your Next.js application for database management - [`GitHub Actions`](https://www.prisma.io/docs/guides/integrations/github-actions): Provision a Prisma Postgres database for every pull request with GitHub Actions and the Prisma CLI, apply your migrations and seed data to it, and delete it when the pull request closes. - [`Permit.io`](https://www.prisma.io/docs/guides/integrations/permit-io): Learn how to implement access control with Prisma ORM with Permit.io - [`pgfence`](https://www.prisma.io/docs/guides/integrations/pgfence): Analyze Prisma Migrate SQL files for dangerous lock patterns, risk levels, and safe rewrite recipes before deploying to production