# 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 the current release. 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) 24 or later
* 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=<id>`), a server `src/app/page.tsx` that redirects to `/?chat=<generateId()>` 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
cd ai-sdk-prisma
```

#### pnpm

```bash
pnpm dlx create-prisma@latest create ai-sdk-prisma --template next --provider postgres
cd ai-sdk-prisma
```

#### yarn

```bash
yarn dlx create-prisma@latest create ai-sdk-prisma --template next --provider postgres
cd ai-sdk-prisma
```

#### npm

```bash
npx create-prisma@latest create ai-sdk-prisma --template next --provider postgres
cd ai-sdk-prisma
```

Answer the prompts for contract authoring style (this guide uses PSL) and package manager. 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 you can open 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="<your connection string>"
```

## 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 temporal.updatedAtString()
  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="<your connection string>"
OPENAI_API_KEY="<your 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<UIMessage[]> {
  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 <Chat id={chat} />;
}
```

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 <p className="empty">Loading...</p>;

  return (
    <main className="shell chat">
      {messages.map((message) => (
        <div key={message.id} className={`bubble ${message.role}`}>
          <p className="eyebrow">{message.role === "user" ? "You" : "AI"}</p>
          {message.parts.map((part, i) =>
            part.type === "text" ? <div key={`${message.id}-${i}`}>{part.text}</div> : null,
          )}
        </div>
      ))}

      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (!input.trim()) return;
          sendMessage({ text: input });
          setInput("");
        }}
      >
        <input
          className="composer"
          value={input}
          placeholder="Say something..."
          onChange={(e) => setInput(e.currentTarget.value)}
        />
      </form>
    </main>
  );
}
```

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=<id>`, 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=<id>"
```

```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.runtime().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.

* `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@latest 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