Chatpack
Core Concepts

Server API

Call the chat engine directly from server code with chat.api.* - every method, and when to use it over HTTP.

chat.api.* is the same domain logic the HTTP routes use, callable directly from your server code. Every method takes an explicit userId and enforces the same permissions.

These are the only methods on chat.api - do not invent others. In particular there is no chat.api.getOrCreateDirectConversation: that name is a low-level storage adapter method. Never call the adapter directly.

Methods

MethodWhat it does
api.getOrCreateConversationFind or create the 1:1 conversation for a user pair
api.listConversationsList a user's conversations, most recent first
api.getConversationFetch one conversation (read-permission checked)
api.sendMessageSend a text message (write-permission checked)
api.listMessagesPaginate history, newest-first
api.editMessageEdit your own message
api.deleteMessageSoft-delete your own message
api.markReadUpdate durable read-state (lastReadMessageId); monotonic - marking an older message is a silent no-op
api.listMessagesAfterMessages after a seq (SSE reconnect gap-fill)

Which API do I call?

The same task, from both sides - chat.api.* in server code, the REST route from a browser or HTTP client:

I want to...Server (chat.api.*)HTTP
Start a chat with someonegetOrCreateConversationPOST /conversations
Show the inbox / sidebarlistConversationsGET /conversations
Open one conversationgetConversationGET /conversations/:id
Load history / scroll backlistMessagesGET /conversations/:id/messages
Send a messagesendMessagePOST /conversations/:id/messages
Edit / delete my messageeditMessage, deleteMessagePATCH / DELETE /messages/:id
Mark a conversation readmarkReadPOST /conversations/:id/read
Get live updates in the browser- (server-sent events)GET /stream via EventSource

Usage

// find-or-create a 1:1 conversation between two users
const conversation = await chat.api.getOrCreateConversation({
  userId: "alice",
  otherUserId: "bob",
});

// send a message
const message = await chat.api.sendMessage({
  userId: "alice",
  conversationId: conversation.id,
  body: "hey bob!",
});

// paginate history (newest first)
const { messages, nextCursor } = await chat.api.listMessages({
  userId: "bob",
  conversationId: conversation.id,
  limit: 50,
});

// durable read-state
await chat.api.markRead({
  userId: "bob",
  conversationId: conversation.id,
  messageId: message.id,
});

Return shapes: bare objects, no envelopes

Server-side methods return the bare object (Conversation, Message, ...). The HTTP layer wraps responses in envelopes ({ conversation }, { message }, { messages, nextCursor }) - that envelope is HTTP-only and intentional (room to add sibling fields without breaking clients). Don't reuse HTTP-response types for chat.api.* calls or vice versa.

Conversation-returning methods (getOrCreateConversation, listConversations, getConversation) return ConversationWithUnread - the conversation plus the calling user's unreadCount (messages newer than their read-state, excluding their own). See Conversations & messages.

Timestamps are real Date instances on the server, ISO strings over HTTP.

Errors: thrown, never null

All failures throw ChatpackError with a stable code - methods never return null for missing resources. api.getConversation throws CONVERSATION_NOT_FOUND; don't confuse it with the storage adapter's getConversation, which returns Conversation | null (core is the layer that turns a null into the domain error).

import { ChatpackError } from "@chatpack/core";

try {
  await chat.api.getConversation({ userId, conversationId });
} catch (err) {
  if (err instanceof ChatpackError && err.code === "CONVERSATION_NOT_FOUND") {
    // handle the missing conversation
  } else {
    throw err;
  }
}

See Error handling for the full code list.

On this page