Skip to main content
The chatbot uses PostgreSQL with Drizzle ORM. All schemas are defined in lib/db/schema.ts.

Tables

User

Stores user accounts and authentication data.
id
uuid
required
Primary key, auto-generated UUID
email
varchar(64)
required
User email address, unique identifier for login
password
varchar(64)
Hashed password (nullable for OAuth-only users)
export const user = pgTable("User", {
  id: uuid("id").primaryKey().notNull().defaultRandom(),
  email: varchar("email", { length: 64 }).notNull(),
  password: varchar("password", { length: 64 }),
});
From lib/db/schema.ts:14-18

Chat

Stores chat conversations.
id
uuid
required
Primary key, auto-generated UUID
createdAt
timestamp
required
When the chat was created
title
text
required
Chat title (auto-generated or user-defined)
userId
uuid
required
Foreign key to User table
visibility
enum
default:"private"
required
Chat visibility: "public" or "private"
export const chat = pgTable("Chat", {
  id: uuid("id").primaryKey().notNull().defaultRandom(),
  createdAt: timestamp("createdAt").notNull(),
  title: text("title").notNull(),
  userId: uuid("userId")
    .notNull()
    .references(() => user.id),
  visibility: varchar("visibility", { enum: ["public", "private"] })
    .notNull()
    .default("private"),
});
From lib/db/schema.ts:22-32

Message_v2

Stores chat messages with support for multimodal content (text and files).
This is the current message schema. The old Message table is deprecated.
id
uuid
required
Primary key, auto-generated UUID
chatId
uuid
required
Foreign key to Chat table
role
varchar
required
Message role: "user", "assistant", or "system"
parts
json
required
Array of message parts (text or file objects)
attachments
json
required
Array of file attachments
createdAt
timestamp
required
When the message was created
export const message = pgTable("Message_v2", {
  id: uuid("id").primaryKey().notNull().defaultRandom(),
  chatId: uuid("chatId")
    .notNull()
    .references(() => chat.id),
  role: varchar("role").notNull(),
  parts: json("parts").notNull(),
  attachments: json("attachments").notNull(),
  createdAt: timestamp("createdAt").notNull(),
});
From lib/db/schema.ts:50-59

Parts structure

The parts field contains an array of content blocks:
{
  "type": "text",
  "text": "What is the weather like today?"
}

Vote_v2

Stores user votes (upvotes/downvotes) on messages.
chatId
uuid
required
Part of composite primary key, foreign key to Chat
messageId
uuid
required
Part of composite primary key, foreign key to Message_v2
isUpvoted
boolean
required
true for upvote, false for downvote
export const vote = pgTable(
  "Vote_v2",
  {
    chatId: uuid("chatId")
      .notNull()
      .references(() => chat.id),
    messageId: uuid("messageId")
      .notNull()
      .references(() => message.id),
    isUpvoted: boolean("isUpvoted").notNull(),
  },
  (table) => {
    return {
      pk: primaryKey({ columns: [table.chatId, table.messageId] }),
    };
  }
);
From lib/db/schema.ts:85-101 Composite Primary Key: (chatId, messageId) ensures one vote per message.

Document

Stores AI-generated documents and artifacts with versioning.
id
uuid
required
Part of composite primary key, auto-generated UUID
createdAt
timestamp
required
Part of composite primary key, enables versioning
title
text
required
Document title
content
text
Document content (nullable)
kind
enum
default:"text"
required
Document type: "text", "code", "image", or "sheet"
userId
uuid
required
Foreign key to User table
export const document = pgTable(
  "Document",
  {
    id: uuid("id").notNull().defaultRandom(),
    createdAt: timestamp("createdAt").notNull(),
    title: text("title").notNull(),
    content: text("content"),
    kind: varchar("text", { enum: ["text", "code", "image", "sheet"] })
      .notNull()
      .default("text"),
    userId: uuid("userId")
      .notNull()
      .references(() => user.id),
  },
  (table) => {
    return {
      pk: primaryKey({ columns: [table.id, table.createdAt] }),
    };
  }
);
From lib/db/schema.ts:105-124 Composite Primary Key: (id, createdAt) allows multiple versions of the same document.

Suggestion

Stores AI-generated editing suggestions for documents.
id
uuid
required
Primary key, auto-generated UUID
documentId
uuid
required
Part of composite foreign key to Document
documentCreatedAt
timestamp
required
Part of composite foreign key to Document, links to specific version
originalText
text
required
Text to be replaced
suggestedText
text
required
Suggested replacement text
description
text
Optional description of the suggestion
isResolved
boolean
default:"false"
required
Whether suggestion has been accepted/rejected
userId
uuid
required
Foreign key to User table
createdAt
timestamp
required
When suggestion was created
export const suggestion = pgTable(
  "Suggestion",
  {
    id: uuid("id").notNull().defaultRandom(),
    documentId: uuid("documentId").notNull(),
    documentCreatedAt: timestamp("documentCreatedAt").notNull(),
    originalText: text("originalText").notNull(),
    suggestedText: text("suggestedText").notNull(),
    description: text("description"),
    isResolved: boolean("isResolved").notNull().default(false),
    userId: uuid("userId")
      .notNull()
      .references(() => user.id),
    createdAt: timestamp("createdAt").notNull(),
  },
  (table) => ({
    pk: primaryKey({ columns: [table.id] }),
    documentRef: foreignKey({
      columns: [table.documentId, table.documentCreatedAt],
      foreignColumns: [document.id, document.createdAt],
    }),
  })
);
From lib/db/schema.ts:128-150 Composite Foreign Key: (documentId, documentCreatedAt) references specific document version.

Stream

Stores resumable stream IDs for chat responses.
id
uuid
required
Primary key, stream identifier
chatId
uuid
required
Foreign key to Chat table
createdAt
timestamp
required
When stream was created
export const stream = pgTable(
  "Stream",
  {
    id: uuid("id").notNull().defaultRandom(),
    chatId: uuid("chatId").notNull(),
    createdAt: timestamp("createdAt").notNull(),
  },
  (table) => ({
    pk: primaryKey({ columns: [table.id] }),
    chatRef: foreignKey({
      columns: [table.chatId],
      foreignColumns: [chat.id],
    }),
  })
);
From lib/db/schema.ts:154-168
Stream records enable resumable streaming when Redis is configured, allowing interrupted streams to be resumed.

Relationships

User relationships

  • One-to-many with Chat: Users can create multiple chats
  • One-to-many with Document: Users can create multiple documents
  • One-to-many with Suggestion: Suggestions belong to document owners

Chat relationships

  • One-to-many with Message: Chats contain multiple messages
  • One-to-many with Vote: Chats can have votes on their messages
  • One-to-many with Stream: Chats can have multiple stream sessions

Document relationships

  • One-to-many with Suggestion: Documents can have multiple suggestions
  • Versioning: Same id with different createdAt creates versions

Cascade deletes

When you delete a chat:
  1. All messages in the chat are deleted
  2. All votes on those messages are deleted
  3. All stream records are deleted
When you delete a document version:
  1. All suggestions for that version are deleted

Deprecated tables

These tables are deprecated and will be removed in a future version. See migration guide at https://chatbot.dev/docs/migration-guides/message-parts

Message (deprecated)

Old message table without multimodal support.
export const messageDeprecated = pgTable("Message", {
  id: uuid("id").primaryKey().notNull().defaultRandom(),
  chatId: uuid("chatId")
    .notNull()
    .references(() => chat.id),
  role: varchar("role").notNull(),
  content: json("content").notNull(),
  createdAt: timestamp("createdAt").notNull(),
});
From lib/db/schema.ts:38-46

Vote (deprecated)

Old vote table referencing deprecated Message table.
export const voteDeprecated = pgTable(
  "Vote",
  {
    chatId: uuid("chatId")
      .notNull()
      .references(() => chat.id),
    messageId: uuid("messageId")
      .notNull()
      .references(() => messageDeprecated.id),
    isUpvoted: boolean("isUpvoted").notNull(),
  },
  (table) => {
    return {
      pk: primaryKey({ columns: [table.chatId, table.messageId] }),
    };
  }
);
From lib/db/schema.ts:65-81

TypeScript types

All tables export TypeScript types for type-safe queries:
export type User = InferSelectModel<typeof user>;
export type Chat = InferSelectModel<typeof chat>;
export type DBMessage = InferSelectModel<typeof message>;
export type Vote = InferSelectModel<typeof vote>;
export type Document = InferSelectModel<typeof document>;
export type Suggestion = InferSelectModel<typeof suggestion>;
export type Stream = InferSelectModel<typeof stream>;
These types are inferred from the schema definitions using Drizzle’s InferSelectModel utility.

Build docs developers (and LLMs) love