ai-sdk-threads
API

Schema

The two tables, every column, and why the timestamps are millisecond precision.

import { messages, threads } from "ai-sdk-threads/drizzle";

They are plain drizzle objects, so they land in your own schema and migration history. The ai_sdk_ prefix keeps them from colliding with your application tables.

ai_sdk_threads

ColumnTypeNotes
idtext PK
user_idtextIndexed. Nullable for anonymous chats.
titletext
visibilitytext'private' (default) or 'public'.
active_leaf_idtextThe last message on the live path.
active_stream_idtextSet while a reply streams; resumableChat resumes from it.
metadatajsonbYours to use.
created_attimestamptz(3)Millisecond precision on purpose - see below.
updated_attimestamptz(3)Moved by appendMessages and updateThread.

ai_sdk_messages

ColumnTypeNotes
idtext PKThe UIMessage id.
thread_idtextIndexed, ON DELETE CASCADE.
parent_idtextThe message this one answers.
roletext'system', 'user', or 'assistant'.
partsjsonbUIMessage.parts, verbatim.
metadatajsonbUIMessage.metadata, verbatim.
sdk_versionsmallintThe ai major that wrote the row.
created_attimestamptz(3)

Why millisecond precision

The timestamp columns are millisecond precision, not Postgres' microsecond default.

listThreads' cursor carries created_at through a JavaScript Date, which cannot represent microseconds. At the default precision the cursor rounds down and the following page silently skips every row sharing that millisecond - so a list of six threads paged to exhaustion could return three, with no error anywhere.

Keep the precision if you hand-write the migration.

Messages form a tree

Each message row points at its parent, so the messages of a thread form a tree rather than a flat list, and the thread's active_leaf_id marks which path through that tree is the live conversation. loadMessages returns exactly that path.

A thread is genuinely a tree once anything has been edited or regenerated. If you query the tables directly, walk parent_id from active_leaf_id - or call orderPath - rather than sorting by created_at. A plain sort interleaves branches that were never part of the same conversation.

sdk_version records which ai major wrote each row. Nothing reads it yet; it is there so a future SDK major can migrate stored parts instead of guessing what shape they are in. See Migrating.

On this page