Chat persistence for the Vercel AI SDK

Threads, message trees, branching and resumable streams in your own database. Zero runtime dependencies.

v0.1.0 on npm

The route

Same behaviour, one call

Both of these are in the docs and both typecheck against the published package on every build. The left one is what the persistence pattern asks you to maintain per app.

by hand25 lines
const { id, messages } = await req.json();

const existing = await store.loadMessages(id);
const known = new Set(existing.map((m) => m.id));
const fresh = messages.filter((m) => m.role === "user" && !known.has(m.id));
if (fresh.length > 0) await store.appendMessages(id, fresh);

const history = [...existing, ...fresh];
const result = streamText({
  model: openai("gpt-5"),
  messages: await convertToModelMessages(history),
});

let persisted = false;
const persist = async ({ responseMessage }) => {
  if (persisted || responseMessage.parts.length === 0) return;
  persisted = true;
  await store.appendMessages(id, [responseMessage]);
};

return result.toUIMessageStreamResponse({
  generateMessageId: generateId,
  onEnd: persist,
  onFinish: persist,
});

Miss generateMessageId and replies store with an empty id. Register only onEnd and nothing persists on ai 6. Forget to filter and every turn duplicates rows.

ai-sdk-threads8 lines
export const POST = chatHandler({
  store,
  execute: ({ modelMessages }) =>
    streamText({
      model: openai("gpt-5"),
      messages: modelMessages,
    }),
});

Plus authorization, branching, and the truncated-reply handling the left pane does not attempt.

Branching

Regenerate, and the old answer is still there

Every message points at its parent, so a regenerated reply is a sibling rather than an overwrite. Nothing is ever deleted.

ai_sdk_messages

user

Explain closures, briefly.

assistant

A closure is a function bundled with the variables it was defined alongside.

1 / 2activeLeafId = a1

An illustration of the stored shape. The buttons are siblingsOf plus setActiveLeaf; nothing in the ecosystem persists this tree. Run it against a real database in your browser.

Coverage

What ai-sdk-threads gives you

One-line chat route
chatHandler replaces the load, store, stream, store boilerplate every AI SDK app writes by hand.
Branching
Edit or regenerate and the old version survives as a sibling, the way ChatGPT does it.
Resumable streams
resumableChat ships the POST, GET and DELETE trio, so a reload mid-answer picks the stream back up.
Postgres or SQLite
One ThreadStore contract over either, verified by a parity suite run against both.
UIMessage-native
Parts and metadata stored verbatim, never flattened to a content string that loses tool calls.
Zero runtime dependencies
ai, drizzle-orm and resumable-stream are peers, the last two optional. Install only what you use.

Your database. Your rows. No service to sign up for.